Should I test that?

Should I test private methods?

Verdict

No

Do not write tests that call private methods directly; test their behaviour through the public methods that call them.

Why

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 the decision changes
WhenDecisionWhy
The public method that calls the private helper has no testsTest minimally: one test of the public method that reaches the helperDetectability moves to same-day, because only a user report reveals the bug
The private method calculates what customers are chargedTest mandatory, through the public method, with boundary valuesBlast radius rises to money and Reversibility to costly
Public tests reach only some branches of the private method, such as dates in UTC onlyTest, after extracting the logic into a class with a public methodDetectability moves to eventually; Test cost falls to trivial after the extraction
The private method decides whether a user may read a recordTest mandatory, through the public method, including the denied casesBlast radius rises to safety-or-legal
The class belongs to a one-off data migrationTest it differently: rehearse the migration on a copy of the dataChange frequency drops to once and Reversibility rises to with-effort
The private helper is a Python function that a test can call without reflectionDo not test the helper directlyTest 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.

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.

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.