Skip to main content

Practical Decisions on Testing: Considering a Unit

Without intending to when I started, I've three posts on a topic now, which I think constitutes a series on "Practical Decisions on Testing". Previously posts have covered:

As with the above two posts, the intention I've primarily got is to put down in words some discussions I've had at various times, for a reference and as something to point to.

In this post I want to discuss what we consider as "the unit" when it comes to unit testing. For many developers, it's the class, and the mark of good coverage is to ensure that all public methods - and via them, the private ones - are covered by tests. In doing so, there will certainly be a well tested code base.

There can be a downside discovered in this though, in that it serves as a deterrent for refactoring at the inter-class level. For example, a decision to split the responsibilities of a class into two to better meet the single responsibility principle, is likely going to lead to quite a number of new and changed tests.

This topic is discussed in a great talk shared by a colleague, by Ian Cooper, titled TDD, where did it all go wrong, which is well worth a watch. It's been a while since I did, but I recall taking from it to consider the unit as something that might be higher level than a single class. In doing so, you keep the same level of code coverage but do so by testing the "outer" public methods of the class aggregation, retaining your ability to quickly refactor without so much burden in updating tests.

I also had chance to consider the recently discussing a PR with a colleague, who was asking where the tests for a new class were. The scenario here can be generalised to the following situation. Say you have a class A, with various public methods that are tested. A may well have various dependencies, which in the case of those that are either brittle or slow, are mocked.

On reflection, we decide class A is too big and an extract class refactoring looks useful, creating class B, which then becomes a dependency of A. A's methods that use the functionality of B, now make calls to it via a level of indirection. And as we had good test coverage on class A, we've been able to do this refactoring with the backup of the tests, thus being confident we haven't introduced any new issues.

The question on the PR then was, "where are the tests on the public methods of class B?", and my argument was, there aren't any, and actually, that's OK. Class A is considered the public API that needs testing, whereas class B is an implementation detail, to be considered from a testing perspective no different from private methods in A. And should we refactor further - perhaps collapsing B back into A, or splitting it's responsibilities further - there's again no need for changes to tests.

Similarly, there's no need to create an interface for B and mock it when testing A - we can, and should, just call into it's methods. So rather than considering that "a unit = a class" and that you should mock all dependencies so you test just that one unit; you can expand the thinking to a unit being a "unit of runnable code without slow/brittle dependencies", making it clear there's no need for mocking these dependencies that are "just more code" and aren't components that without mocking would lead to a flaky or slow test base.

Broadly that's the message of Ian's talk as I recall, but I think there's another nuance to it - which is that, whilst it's justified and actually beneficial to avoid writing unit tests for class B's public methods, depending on how this class gets used in the future, that may not always be the case.

Let's say for example class C is developed and also takes a dependency on B, calling into it's methods. Class C has public methods that are unit tested, so we're still all good from a code coverage perspective. We can even extend B, adding some feature that C needs, and with the tests on A and C we're still fully covered. There surely comes a point though when the importance of B to the code-base has increased beyond it's original "helper" status. With more classes referencing and depending on it, it's been upgraded to more of an "application service", and somewhere along this path we'll likely hit a point where we do want to have tests dedicated to B.

Slightly less abstractly, if we consider that A and C are controllers calling into B that was a class holding some business logic, we might well be able to right cleaner, clearer and more thorough tests if we focused directly on the methods and logic of B.

As if often the case with software development best practices and principles, with this topic there's not a black and white rule to follow. As usual, judgement and experience are called in in making another practical decision around when and where to test.