Verdict
No
Do not write tests that call private methods directly; test their behaviour through the public methods that call them.
Why
- Blast radiususers
- Change frequencyregularly
- Detectabilityimmediately
- Reversibilitytrivial
- Test costmoderate
Do not write tests that call private methods directly; test their behaviour through the public methods that call them. In the typical case, a helper in a service class whose public caller has unit tests, Blast radius is users and Change frequency is regularly, because a wrong result reaches customers and refactors reshape helpers about once a month. Detectability is immediately and Reversibility is trivial, because a broken helper fails the tests of its public caller in CI and the change never merges. Test cost is moderate, because a direct test needs reflection or a wider visibility modifier and must be rewritten when the helper changes shape. Rule R8 gives Do not test for this combination.
| When | Decision | Why |
|---|---|---|
| The public method that calls the private helper has no tests | Test minimally: one test of the public method that reaches the helper | Detectability moves to same-day, because only a user report reveals the bug |
| The private method calculates what customers are charged | Test mandatory, through the public method, with boundary values | Blast radius rises to money and Reversibility to costly |
| Public tests reach only some branches of the private method, such as dates in UTC only | Test, after extracting the logic into a class with a public method | Detectability moves to eventually; Test cost falls to trivial after the extraction |
| The private method decides whether a user may read a record | Test mandatory, through the public method, including the denied cases | Blast radius rises to safety-or-legal |
| The class belongs to a one-off data migration | Test it differently: rehearse the migration on a copy of the data | Change frequency drops to once and Reversibility rises to with-effort |
| The private helper is a Python function that a test can call without reflection | Do not test the helper directly | Test cost drops to trivial, but the public tests still fail at once |
What breaks if you don't test
When a public test reaches the helper, a wrong value from the helper becomes a wrong value from the public method, and that test fails. The risk sits in helper branches that no public test reaches. A private helper that formats a delivery date in the customer's time zone, tested only with UTC inputs, can print the wrong day on an order confirmation until a customer complains.
What you lose if you over-test
A direct test pins the structure of the class instead of its behaviour. Rename the helper, merge it with another, or inline it, and the test fails although customers get the same result. The larger loss is false confidence: a direct test proves that the helper works, not that the public method still calls it. In JavaScript a #private method cannot be reached from a test, so teams make it public for the test, and other code starts to depend on it.
What to do instead
Write unit tests at the level of the public method. Pick inputs that drive each branch of the private method, and assert on the public result: the returned value, the saved record, or the response. If no public input reaches a branch, the branch is dead code or a second responsibility. Delete dead code, and move a second responsibility into its own class with a public method that you test directly. The JUnit user guide and the Python unittest documentation show how to write tests against a public API.
Procedure and references
When the answer changes
- The private method calculates an amount that customers pay or receive.
- The private method decides who may read or change a record.
- The public tests keep passing when you break the helper on purpose.
Real incident + Code example
The rounding helper that took its test with it
On an invoicing service I worked on, a private helper rounded line totals to cents with half-up rounding, and the only test of that rounding called the helper through reflection. The public tests of total() used whole-cent prices, so none reached the rounding. A refactor replaced the helper with a shared Money.round() and deleted the helper with its test. The shared method used the Java HALF_EVEN mode, which rounds an exact half cent to the even neighbour, so a line of 1.125 became 1.12 instead of 1.13. The build stayed green, and an accountant found the one-cent differences weeks later. The fix was one test at the public level:
@Test
void totalRoundsHalfCentUp() {
// 3 x 0.375 = 1.125, which half-up rounding turns into 1.13
Invoice invoice = new Invoice(List.of(new Line(3, new BigDecimal("0.375"))));
assertEquals(new BigDecimal("1.13"), invoice.total());
}
The test fails whenever the invoice total is wrong, wherever the rounding lives.
Related questions
FAQ
- Should I unit test private methods?
No, unit test private methods through the public methods that call them, with inputs that reach every branch of the private code. A direct test of a private method breaks when the class is refactored without any change in behaviour.
- Should I unit test private functions?
No, test the private functions of a module through the exported functions that call them. If exported inputs cannot reach all of a private function's logic, move it into its own module and export it.
- How do I test a private method without making it public?
Call the public method with inputs that reach the private code path, and assert on the public result. If no public input reaches the path, extract the logic into a separate class with a public method and test that class.
- Is testing private methods a code smell?
Yes, a need to test a private method directly often signals that the class holds two responsibilities. Extracting the second responsibility into its own class gives it a public interface that you can test without reflection.