Principle 2: be sociable
Another reason for your test to fail 🔴 unnecessarily is when your tests rely too heavily on mocks or test implementation details that are prone to frequent changes.
testing low-level abstraction code
Low-level abstraction code is close to the implementation details, and that is the one that changes the most often. Reasons for changes include:
Refactoring the model to match new requirements
Implementation changes causing methods to disappear
Extracting new abstraction layers (for example to unify some constraints or behavior)
It’s pretty intuitive that low level code has a risk of changing often due to the reasons above. Tests covering low-level component will then become obsolete as soon as the implementation changes.
So if we want to test some higher-level code instead, we tend to use mocks to mock the dependencies of the code under test.
why mocks suck
There is one huge problem with mocks - they are actually one of the biggest reasons your tests are coupled to the implementation.
Mocks actually perfectly reflect your implementation details. They know EVERYTHING about your low-level component structure and exactly mimic their usage. They mimic when, how many times and with what parameters they should be called.
If you’ve been using mocks in the past - how often changing method return type breaks unrelated tests?
alternative to mocks - sociable tests
There are some approaches that limit mocks negative effects - and one is using sociable tests.
Sociable tests interact with multiple components or units of code, as shown on the left side of the picture. First choose the unit of code you want to test . Then instead of mocking its dependencies, construct your unit in tests the way you’d use it on production servers.
The other side of the coin is solitary tests - they test a single unit of code in isolation, mocking out all dependencies.
why sociable tests
Advice is simple - use sociable tests as much as possible.
This way, even if you change the code somewhere in the middle of your call chain, your tests will still pass. Your tests will not construct mocks of code that can change often.
This actually decouples your tests from the implementation details. Tests should know nothing about the classes, objects and functions in the middle of the call chain.
How would it look in a more real-life example? Imagine you have a REST interface for your system’s functionality. First, an example of using a solitary test:
Now if you’d approach testing with sociable tests, choose a higher-level unit of code and test it with its real dependencies like this:
NOTE: You’ll still use mocks (or other test doubles) at the edges of your system (IO operations like database or HTTP clients). In the future, we’ll see how to get rid of mocks in that area of code as well!
Final thoughts
Remember to use sociable tests as much as possible. They are more stable and less prone to changes when your implementation details change.