Test Double HeuristicsJune 3, 2020
In the previous blog post, we’ve talked about avoiding excessive specification of test doubles. This is just one in a series of “good practices” for using test doubles in solitary tests. Let’s have a look at a few more guidelines that increase the maintainability of solitary tests when using test doubles.
Avoid using test doubles for types that you don’t own
Suppose that we’re using a third-party library in our application. We have some solitary tests that uses test doubles for some interface types that are exposed through the public API.
When upgrading this third-party library to a newer version, we might run into some subtle and possibly less subtle issues whenever breaking changes have been introduced by its maintainers. This implies that we potentially have to fix several of our solitary tests every time we perform such an upgrade.
Using test doubles for types that are not under our control might also be a good indication that the current design is too strongly coupled to a third-party library. Another issue might be that due to the potential complexity of said library, a large amount of test doubles need to be configured which brings us back to the realm of complex test fixtures and excessive specification. We want to steer clear from these kinds of situations whenever possible.
A solution is to introduce an adapter layer that isolates the use of the third-party library from the rest of the application. Breaking changes in the third-party library are now limited to the sociable tests for this adapter layer. These adapters provide us with our own interface that we can control and which we are able to substitute when needed.
Avoid test doubles for concrete classes
Creating test doubles for concrete classes is a technique that should be avoided unless it’s absolutely necessary. When designing and writing new code, make sure to create test doubles for interfaces instead of concrete classes. Creating a test double of a concrete class might only be feasible when you’re working in a legacy application where you need to swap out a concrete collaborator.
One way to approach this is to manually derive a class and override the necessary methods. This newly derived class can then be used as a test double by the solitary tests. Note that this is usually only a temporary solution. After further refactoring the code, at some point, an interface can be introduced which removes the need for the concrete test double.
So the general guideline is to stick with creating test doubles for roles, not concrete classes.
Don’t let test doubles return other test doubles
We can be very brief when it comes to this guideline. Whenever you encounter the need to let a test double return an instance of another test double, then this is usually a clear sign to reconsider the design of the system instead. Test doubles that return other test doubles is considered an anti-pattern.
Don’t implement behaviour in test doubles
Implementing behaviour in test doubles becomes quite problematic really fast. Let’s have a look at an example.
HeadOfDepartment headOfDepartment = Example.HeadOfDepartment()
var approverRepository = Substitute.For<IApproverRepository>();
approverRepository.Get(Arg.Any<Guid>()).Returns( callInfo =>
var id = callInfo.Arg<Guid>();
return id != Guid.Empty ? headOfDepartment : null;
Here we’re using the NSubstitute library to create a stub for the IApproverRepository interface. Instead of just returning a value when the Get method of the stub is called, we specify a callback lambda function. This function first retrieves the value of the specified parameter. In this case it’s the identifier (GUID) for an approver. If the specified identifier is any GUID, then an instance of an HeadOfDepartment is returned. Otherwise, a null reference is returned for an empty GUID.
At first sight there doesn’t seem to be anything wrong. However, a big issue arises when the real implementation of the IApproverRepository implements a different behaviour. For example, it might not return a null reference for an empty GUID identifier. This implies that the Subject Under Test is tested against incorrect behaviour.
Therefore, it’s better to avoid implementing behaviour in test doubles like dummies, stubs, spies and mocks. However, there’s one type of test double that is an exception to this guideline. A Fake is a test double for which it is perfectly fine to implement behaviour. In fact, that’s the whole point of having a fake object. As we already mentioned in a previous blog post, fake objects are not well suited for solitary tests anyway.
Reduce the number of collaborators
Every time we need to instantiate a vast number of test doubles in order to create an instance of the Subject Under Test, then we need to reflect once more on the design of the system. When applying the dependency injection pattern, a bloated constructor is usually a clear sign the Subject Under Test has too many responsibilities. Such a violation of the Single Responsibility Principle can be mitigated by extracting functionality into separate classes.
We should then look for collaborators that are always used together to get a sense of the parts of the implementation that can be grouped and extracted behind a more course-grained interface. Without imposing a hard number, because there can be exceptions, whenever a Subject Under Test has more than five different collaborators then this might be an indication of a potential design issue. Be very mindful about this, especially when the need arises to add even more collaborators.
These are a couple of guidelines that I find useful when working with test doubles.
If you and your team want to learn more about how to write maintainable unit tests and get the most out of TDD practices, make sure to have look at our trainings and workshops or check out the books section. Feel free to reach out at info. @ principal-it .be
Jan Van Ryswyck
Thank you for visiting my blog. I’m a professional software developer since Y2K. A blogger since Y2K+5. Provider of training and coaching in XP practices. Curator of the Awesome Talks list. Past organizer of the European Virtual ALT.NET meetings. Thinking and learning about all kinds of technologies since forever.
Watch The Videos
- Behavior-Driven Development
- Concurrent Programming
- Continuous Integration
- Core Skills
- Design Patterns
- Domain-Driven Design
- Event Sourcing
- Fluent Interfaces
- Functional Programming
- Object-Relational Mapping
- Open Source
- Software Design
- Test-Driven Development
- Visual Studio
The opinions expressed on this blog are my own personal opinions. These do NOT represent anyone else’s view on the world in any way whatsoever.
Thank you for visiting my website. I’m a professional software developer since Y2K. A blogger since Y2K+5. Author of Writing Maintainable Unit Tests. Provider of training and coaching in XP practices. Curator of the Awesome Talks list. Thinking and learning about all kinds of technologies since forever.