Chapter 5. Mocks, Stubs, Test Spies
5.5. Always Use Test Doubles… or Maybe Not?
One feature of a decent unit test is isolation (see Section 2.1). Testing in isolation allows one to find out whether the SUT behaves properly, independently of the implementation of other classes. This is an important property of unit tests, that should not be recklessly abandoned. In previous sections we have learned how to use test doubles to fully control the SUT’s collaborators.
But does this mean that you should always create a test double for every collaborator of a class?
If it should turn out that using a real collaborator means involving database connections, third-party frameworks or costly computations in the conducting of unit tests, then have no choice: you must use a test double – otherwise it will not be a unit test anymore10. Using a real class would make your (supposed) unit test depend on too many external components – ones that could, in turn, make the test fail unexpectedly, even if your code has worked fine. This is unacceptable for unit tests.
But what if a collaborator is a very simple class with almost no logic? Is replacing it with a test double worth the effort? Let us look for an answer to this question by considering the example of two simple classes shown below.
Listing 5.37. Phone class
public class Phone {
private final boolean mobile;
private final String number;
public Phone(String number, boolean mobile) { this.number = number;
this.mobile = mobile;
}
Chapter 5. Mocks, Stubs, Test Spies
Listing 5.38. Client class
public class Client {
private final List<Phone> phones = new ArrayList<Phone>();
public void addPhone(Phone phone) { phones.add(phone);
}
public boolean hasMobile() { for (Phone phone : phones) { if (phone.isMobile()) { return true;
} }
return false;
} }
This is the method that we will be testing in the sections below.
As you can see, the Client class is tightly coupled with the Phone class. There is no interface that would shield Phone from Client.
Now let us think about how we could test the hasMobile() method of the Client class. You would need a few test cases capable of verifying the following:
• if the client has no phones, the hasMobile() method returns false,
• if the client has only stationary (i.e. landline) phones, the hasMobile() method returns false,
• if the client has one or more mobile phones (and any number of stationary phones), the hasMobile()
method returns true.
For our purposes, we shall limit the number of phones to one mobile and one stationary one. This will be sufficient to be illustrative of the case in point.
5.5.1. No Test Doubles
In such a simple case you might be tempted to use the classes directly in your test code. The thing is, making Phone a mobile phone is very easy: simply pass true to its constructor. So you can create instances of the Phone class in test code, as shown in Listing 5.39.
Chapter 5. Mocks, Stubs, Test Spies
Listing 5.39. No test doubles used
public class ClientTest {
final static String ANY_NUMBER = "999-888-777";
final static Phone MOBILE_PHONE = new Phone(ANY_NUMBER, true);
final static Phone STATIONARY_PHONE = new Phone(ANY_NUMBER, false);
Client client = new Client();
@Test
public void shouldReturnTrueIfClientHasMobile() { client.addPhone(MOBILE_PHONE);
client.addPhone(STATIONARY_PHONE);
assertTrue(client.hasMobile());
} @Test
public void shouldReturnFalseIfClientHasNoMobile() { client.addPhone(STATIONARY_PHONE);
assertFalse(client.hasMobile());
} }
Real objects are created to be used by the SUT.
Both test methods use real objects of the Phone class, and both rely on its correctness.
The test code shown in Listing 5.39 is clear and concise. The required DOCs are created using a Phone
class constructor with the appropriate boolean parameter - true for mobiles and false for stationary phones.
5.5.2. Using Test Doubles
The alternative approach would be to use test doubles instead of real objects. This is shown in Listing 5.40.
Listing 5.40. Test doubles
public class ClientTest {
final static Phone MOBILE_PHONE = mock(Phone.class);
final static Phone STATIONARY_PHONE = mock(Phone.class);
Client client = new Client();
@Test
public void shouldReturnTrueIfClientHasMobile() { when(MOBILE_PHONE.isMobile()).thenReturn(true);
client.addPhone(MOBILE_PHONE);
client.addPhone(STATIONARY_PHONE);
assertTrue(client.hasMobile());
} @Test
public void shouldReturnFalseIfClientHasNoMobile() {
Chapter 5. Mocks, Stubs, Test Spies
In contrast to real objects of the Phone class, test doubles have no idea about how to behave, so we need to instruct them. This is required for mobile phones (which should return true). For stationary phones, there is no need to specify a returned value, as mocks created by Mockito return false
by default11.
The code in Listing 5.40 does not differ much from the previously shown Listing 5.39. Constructor calls, which defined how objects of the Phone class should behave, were replaced with calls to Mockito’s
mock() and when() methods.
No Winner So Far
So far, so good. Both approaches seems fine. Using real classes in test code seems to be justified by the close relationship between Client and Phone. Both test classes are concise and free of any logic. Good.
5.5.3. A More Complicated Example
But let us stir things up a little bit, where this very solid construction is concerned, by introducing a small change to the Phone class: let us make it behave more intelligently. Phone constructor can recognize if a number belongs to a mobile phone, using pattern matching. Because of this change, there is no need for the constructor’s second boolean parameter, as shown in Listing 5.41.
Listing 5.41. Phone class constructor enhanced
public Phone(String number) { this.number = number;
this.mobile = number.startsWith("+") && number.endsWith("9");
}
Do not do this at home! This is surely not a valid way to recognize mobile phone numbers!
After this change has been introduced, the test, which does not use mocks, needs to be updated. This time, in order to create appropriate phones (mobile and stationary), a knowledge of the internals of the
Phone class is required. Without it there is no way a developer could construct a phone of the desired type. The test starts to look as shown in Listing 5.42.
11See Section 5.1.1 for information concerning the default behaviour of Mockito’s test doubles.
Chapter 5. Mocks, Stubs, Test Spies
Listing 5.42. No test doubles used - enhanced Phone class version
public class ClientTest {
private final static Phone MOBILE_PHONE = new Phone("+123456789");
private final static Phone STATIONARY_PHONE = new Phone("123123123");
private Client client = new Client();
@Test
public void shouldReturnTrueIfClientHasMobile() { client.addPhone(MOBILE_PHONE);
client.addPhone(STATIONARY_PHONE);
assertTrue(client.hasMobile());
} @Test
public void shouldReturnFalseIfClientHasNoMobile() { client.addPhone(STATIONARY_PHONE);
assertFalse(client.hasMobile());
} }
The chosen phone numbers must follow the logic of the Phone's constructor.
This version of the ClientTest class is coupled with the implementation of the Phone class. If the pattern- matching mechanism used in the Phone constructor changes, the test code will also need to change.
The SRP principle has clearly been breached, because this test class is also coupled with the Client
class implementation (so ClientTest has more than one reason to change). This is a warning signal.
The violation of SRP entails that the DRY principle has also been breached. Surely, there must exist a
PhoneTest class that will make sure that the Phone constructor and isMobile() method works as expected!
To test this functionality, PhoneTest needs to create identical (or almost identical) instances of phones, as shown in Listing 5.42. If so, then a change in the Phone class will entail changes in two tests - PhoneTest
and ClientTest. This is bad.
Surprisingly (or, rather… perhaps not!), the test class based on test doubles remained unchanged. The stubs did not even notice the change of algorithm within the Phone class or the difference in the values of the arguments passed to the constructor, because they do not make use of either of these. They were ordered to return certain values at certain points of execution, and they are still following those orders.
5.5.4. Use Test Doubles or Not? - Conclusion
This issue is worth discussing only in the case of "real" objects: that is, objects that have some business logic and offer some complex behaviour. In the case of DTOs12, or Value Objects13, using a test double will be overkill. Similarly, creating a test double of a java.util.ArrayList
is not recommended.
As has been confirmed in some of the preceding paragraphs, testing without test doubles is possible, but carries some serious consequences. First of all, it may result in cluttering up your test code with
Chapter 5. Mocks, Stubs, Test Spies
has a ripple effect on your tests, with many of them then needing to be changed14. Thirdly, it obliges you to develop classes in some specific order (e.g. the Phone class must be ready and fully tested before you can start working on the Client class), and may lull you into being over-reliant upon the existing implementation of the collaborators.
On the other hand, using test doubles in some situations might be considered overkill. For people who are new to test doubles, writing new MyClass() instead of mock(MyClass.class) is much more natural, especially if there is no instant gain in using test doubles.
In general, I would recommend using test doubles. The overheads related to their creation and the setting of expectations might seem unjustified at first (especially if a collaborator is very simple). However, when the design of your classes changes, you will benefit from the isolation, and your tests will not break down unexpectedly. Also, current frameworks only call for you to write a very small number of lines of code, so there is no decline in productivity. Using a test double makes it virtually impossible to rely on the DOCs’ implementation, as it might not exist yet.
The only situations where I would consider using a real collaborator instead of a test double are the following:
• the collaborator is very, very simple, preferably without any logic (e.g. some sort of "container" class with only accessors and mutators methods),
• the collaborator’s logic is so simple that it is clear how to set it in the desired state (and its logic will not be enhanced in the foreseeable future).
Even then, I would be highly cautious, as changes are inevitable – no matter how very unlikely they may seem!