Verdict
Yes
Test a public static method that contains logic with direct unit tests: call it with the main input and the edge inputs most likely to break, and assert on the return value.
Why
- Blast radiususers
- Change frequencyrarely
- Detectabilityeventually
- Reversibilitywith-effort
- Test costtrivial
Test a public static method that contains logic with direct unit tests. In the typical case, a static helper that parses or formats values for several callers, Blast radius is users and Change frequency is rarely, because a wrong result reaches customers through every caller and shared helpers change a few times a year. Detectability is eventually and Reversibility is with-effort, because caller tests seldom try edge inputs such as a leap day, and callers store the wrong result, which needs a repair script. Test cost is trivial, because a static method with no hidden state needs no instance and no mock. Rule R11 gives Test.
| When | Decision | Why |
|---|---|---|
| The static method is a factory that only passes arguments of different types to a constructor | Do not test the factory; caller tests cover it | Detectability moves to immediately and Reversibility to trivial, because every caller test fails when the factory breaks |
| The static method is a factory that passes several arguments of one type to a constructor | Test: one test that passes a distinct value for each argument and reads each field back | Reversibility falls to trivial, but Detectability stays eventually, because a swapped argument stores a plausible value of the right type |
| The static method is a private helper that tests of a public method already reach with data that gives each branch a different result | Do not test the helper directly; test the public method | Detectability moves to immediately and Reversibility to trivial, while Test cost rises to moderate because a direct test needs reflection |
| The static method reads the system clock or a static cache, and other code depends on its signature | Test minimally: one test of the main path with a fixed clock or a seeded cache | Test cost rises to moderate, because each test must first take control of the clock or cache |
| The static method calculates tax or converts currencies for invoices | Test mandatory, with rounding cases and boundary values | Blast radius rises to money and Reversibility to costly |
| The static method masks email addresses before they reach the logs | Test mandatory, including empty and short addresses | Blast radius rises to safety-or-legal, Detectability to never and Reversibility to impossible: an address copied into log storage cannot be taken back |
What breaks if you don't test
One bug in a static helper appears at every caller at once. Suppose a change makes Phone.normalize(String) add the country code to numbers that already have it, so +44 7700 900123 becomes +44 44 7700 900123. Every signup form and import job that calls the helper saves a broken number. Verification texts stop arriving, and support sees the pattern days after the release.
What you lose if you over-test
A test for a factory whose arguments all differ in type repeats the caller tests. When caller tests replace pure static helpers with mockStatic in Mockito, they pin which helper each caller uses. Move or inline the helper, and every caller test that mocked it fails while customers see the same behaviour. The mock also keeps returning the old answer, so a bug in the helper never reaches the caller tests.
How to test
Call the static method directly, with no test doubles. The minimum set is one test of the main path plus one test per edge input likely to break: null input, boundaries such as the last day of a month, and input the method must reject. A JUnit parameterized test keeps inputs and expected outputs in one table. When a static method reads the clock, pass the value in as a parameter; the .NET unit testing guidance shows this seam for DateTime.Now.
When the answer changes
- The static method calculates an amount that customers pay.
- The static method masks or exports personal data.
- The static method only forwards arguments of different types to a constructor.
Code example
The expiry check that read the clock
The first version of this card check reads the clock:
// Before: the result depends on the month the test runs
static boolean isExpired(Card card) {
return card.expiry().isBefore(YearMonth.now());
}
// After: the caller passes the current month in
static boolean isExpired(Card card, YearMonth today) {
return card.expiry().isBefore(today);
}
@Test
void cardIsValidUntilTheEndOfItsExpiryMonth() {
Card card = new Card(YearMonth.of(2026, 9));
assertFalse(Cards.isExpired(card, YearMonth.of(2026, 9)));
assertTrue(Cards.isExpired(card, YearMonth.of(2026, 10)));
}
A test of the first version with fixed dates fails from October 2026. The second version stays static: production code passes YearMonth.now(), and the test pins the boundary of the expiry month.
Related questions
FAQ
- Should you unit test static methods?
Yes, unit test a public static method that contains logic by calling it and asserting on its return value. A static method with no hidden state needs no instance and no mocks.
- Should I test static factory methods?
Test a static factory method when it validates its arguments or passes several arguments of one type, because a swapped start and end date stores a plausible range that caller tests accept. Skip a factory whose arguments all differ in type and let caller tests cover it, since the compiler rejects a swap.
- How do I test code that calls a static method?
Let a pure static method run inside the test of the calling code and assert on the caller's result. When the static method reads the clock or the network, pass that value in as a parameter instead.
- Are static methods bad for testability?
Static methods that depend only on their arguments are easy to test, because a test calls them with no setup. Static methods that read the system clock or a static cache make themselves and their callers hard to test until that state moves into parameters.
- Should I mock static methods?
Mock a static method only when it reaches something a test cannot control and you cannot change its code. Let a pure static helper run, because a mock of it hides its bugs from caller tests.