From my experience, when introducing mocks to your testing, it is easy to get messy, hard to maintain code. The 4 heuristics I’d like to present come from my experience with Outside-In designed artifacts that might not be right. Keep in mind that these are not meant to be a path of virtue that I must slavishly follow. I often violate them and still feel that the code is good (enough).
Short introduction to Outside-In
The Outside-In approach, often also called the London School, is where you are more focused on testing the behavior of your software instead of its state as you would do in the Classicist approach. In order to test the behavior, you use a mocking framework for your dependencies such as Mockito. I find this approach very useful when writing code that operates on a higher level to glue different parts of my features together1. There is a great article from Sandro Mancuso describing the differences between Outside-In and Classicist approaches: Does TDD really lead to good design?
Rule 1: 2-5 dependencies are good
I think the optimal number of dependencies any SUT may have without immediate need for refactoring is somewhere between two and five dependencies. Two to three are great, and four to five slightly worse, but still okay. In most cases when I need even more I find that:
- the SUT is doing too much (violation of SRP). It is on the verge of becoming a god class. I try to split the code into separate instances with well-defined smaller responsibilities.
- the SUT uses a bunch of low-level dependencies that could be bundled into a single, higher-level dependency (violation of SLA).
Rule 2: A Single dependency is suspicious
A SUT that uses only one dependency is doing something wrong. In my understanding a SUT with dependencies should act as a collaborator. It should pass the outputs of one dependency to another one. How can this work if there is only one dependency?
Whenever I find a class with only one dependency, it usually violates one of the following principles of Clean Code:
- SRP: The dependency might do too many things. Could it be split into two separate instances?
- KISS: The dependency could be inlined into a method inside the SUT reducing the number of instances involved in the process from two to one?
- SLA: The SUT might delegate one thing into its depency and do some other things by itself. Maybe these things could be extracted into new dependencies?
There is one case when I find one dependency acceptable: writing facades around problematic third party APIs to easify their testing or reduce a monstrous feature set to the subset I actually need.
Rule 3: LoC and number of dependencies should be inversely proportional
A high value of lines of code inside a SUT is an indication that it contains much business logic. I’d like to focus on testing it without gazillions lines of cumbersome mock setup for its dependencies. A SUT that has 5 dependencies should not have much more than 100 lines. SUTs with less dependencies might be longer2.
Any class that has both many lines of code and dependencies is IMO legacy code, even if its coverage is 100% and is my top candidate for the next refactoring session 😉
Rule 4: the optimum number of dependencies is zero
Captain obvious once said that testing any code with dependencies is hard. I tend to agree. If I can find any way to extract code into a separate entity without any dependencies I will do so (e.g. extract method, extract class). This helps me to keep the code that glues different parts of my software together minimal and easy to test. My more complex business rules go into an entity that does not depend on anything.