Picture this situation. You have a code base at hand with a complicated dependency graph, where the class you want to test has dependencies, each of which relies on dependencies of its own, and so on, several layers deep:
![[Enable Images]](https://khorikov.org/images/2020/2020-03-07-graph-1.png)
These classes may even introduce circular dependencies, where the chain of dependency eventually comes back to where it started.
How do you even start to approach unit testing all this mess?
Without mocks, testing such an interconnected code base is hard. Pretty much the only choice you are left with is re-creating the full object graph in the test, which might not be feasible if that graph is too large.
On the other hand, with mocks, you can split that object graph. You can substitute the immediate dependencies of the class you want to test; and, by extension, you don’t have to deal with the dependencies of those dependencies, and so on down the recursion path.
You are effectively breaking up the web of dependencies — and that can significantly reduce the number of preparations you have to do in a unit test:
![[Enable Images]](https://khorikov.org/images/2020/2020-03-07-graph-2.png)
It’s understandable why many programmers rely on mocks to simplify unit testing. But make no mistake — it’s a trap. This is the road to brittle tests, paved with good intentions.
I call this the unit testing mistake #3:
"Relying on mocks too much."
The reason why mocks often lead to brittle tests is because of over-specification. They couple tests to the class’s implementation details, as opposed to its observable behavior.
As a result, the tests fail every time you modify those implementation details (that is, after each refactoring), flooding you with false alarms.
Remember: the way each individual class interacts with neighboring classes has nothing to do with its observable behavior. It’s an implementation detail.
Coupling tests to such details increases the noise component in the below formula so much that the test’s accuracy falls to zero, making that test effectively worthless:
![[Enable Images]](https://khorikov.org/images/2020/2020-03-06-signal-to-noise.png)
Instead of tracking interactions between classes, your tests must check the class’s observable behavior — the end result, which usually is a combination of the system’s state and the returned value.
But how to do that when you’ve got such a large graph of interconnected classes?
You should focus on not having such a graph of classes in the first place. More often than not, large graphs of classes are a result of a code design problem.
If you see that to unit test a class, you need to extend the test’s arrange phase beyond all reasonable limits, it’s a certain sign of trouble. The use of mocks only hides this problem; it doesn’t tackle the root cause.
To tackle the root cause, separate code that contains business logic from the code that does orchestration. This will help you reduce the number of dependencies you need to emulate, which, in turn, will eliminate the need for mocks.
Try to always adhere to the following guideline: the more important or complex the code, the fewer collaborators it should have.
Here’s a good way to visualize this guideline:
![[Enable Images]](https://khorikov.org/images/2020/2020-03-07-code-depth-width.png)
You can think of business logic and orchestration responsibilities in terms of code depth versus code width. Your code can be either deep (complex or important) or wide (work with many collaborators), but never both.
For more information about mocking best practices, including an in-depth discussion about scenarios in which mocks are useful (yep, there are such scenarios), go to:
(Use the nwsentr40 code during checkout on the Manning book page to get a 40% discount.)
Up next: more ways to shoot yourself in the foot by increasing the noise component in the signal-to-noise ratio.
Vladimir Khorikov