Yes you should test private methods, wherever possible. Why? To avoid an unnecessary state space explosion of test cases which ultimately just end up implicitly testing the same private functions repeatedly on the same inputs. Let's explain why with an example.
Consider the following slightly contrived example. Suppose we want to expose publicly a function that takes 3 integers and returns true if and only if those 3 integers are all prime. We might implement it like this:
public bool allPrime(int a, int b, int c)
{
return andAll(isPrime(a), isPrime(b), isPrime(c))
}
private bool andAll(bool... boolArray)
{
foreach (bool b in boolArray)
{
if(b == false) return false;
}
return true;
}
private bool isPrime(int x){
//Implementation to go here. Sorry if you were expecting a prime sieve.
}
Now, if we were to take the strict approach that only public functions should be tested, we'd only be allowed to test allPrime
and not isPrime
or andAll
.
As a tester, we might be interested in five possibilities for each argument: < 0
, = 0
, = 1
, prime > 1
, not prime > 1
. But to be thorough, we'd have to also see how every combination of the arguments plays together. So that's 5*5*5
= 125 test cases we'd need to thoroughly test this function, according to our intuitions.
On the other hand, if we were allowed to test the private functions, we could cover as much ground with fewer test cases. We'd need only 5 test cases to test isPrime
to the same level as our previous intuition. And by the small scope hypothesis proposed by Daniel Jackson, we'd only need to test the andAll
function up to a small length e.g. 3 or 4. Which would be at most 16 more tests. So 21 tests in total. Instead of 125. Of course, we probably would want to run a few tests on allPrime
, but we wouldn't feel so obliged to cover exhaustively all 125 combinations of input scenarios we said we cared about. Just a few happy paths.
A contrived example, for sure, but it was necessary for a clear demonstration. And the pattern extends to real software. Private functions are usually the lowest level building blocks, and are thus often combined together to yield higher level logic. Meaning at higher levels, we have more repetitions of the lower level stuff due to the various combinations.