loading...

Test Double Heuristics

janvanryswyck profile image Jan Van Ryswyck Originally published at principal-it.eu on ・4 min read

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.

Third-party library used in application

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.

Third-party library encapsulated by adapter

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.

Test double for concrete class

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.

Test double that returns another test double

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()
    .WithId(new Guid("224FE5B8-EDBB-4F8B-8654-715C1C294CFD"));

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.

Posted on by:

janvanryswyck profile

Jan Van Ryswyck

@janvanryswyck

Owner @ Principal IT, husband, father of three, geek, enjoys running, fan boy of many things but nothing in particular.

Discussion

pic
Editor guide