Test Doubles: creating a Test Stub
In this series of posts about Test Doubles we’ve looked at the manual and tool-aided creation of a Dummy Object. Now it’s time to discuss the creation of a stub. The stub is defined as follows.
A Test Stub is an Object that replaces a real component on which the SUT depends so that the test can control the indirect inputs of the SUT. It allows the test to force the SUT down paths it might not otherwise exercise.
Reasons for using a stub may vary. Some (not all) valid motivations for stubbing are that the real component:
- talks to the database;
- communicates across the network;
- touches the file system;
- performs too slowly to be part of a unit test;
- costs money to use;
- is not implemented (yet).
The first three reasons are based on Micheal Feathers’ list of characteristics that make a test no unit test. A slow unit test – reason four – does not live up to “Fast” expressed in the mnemonic FIRST. Slow is not specific, but a way to decide that a test is slow is to look at it proportionately. How much slower is it than the average runtime of other simpler unit tests?
But even if we choose not to adhere to the previously mentioned principles, we are still left with reason five and six. Imagine a paid service such as a damage history lookup service for car insurance. The decision to stub that service is a no-brainer.
When there is no implementation (yet), we have no choice but to make a stub. Such a situation can occur when we apply a test first approach.
We’ll look at creating a stub for the sixth reason: no implementation yet. We’ll discuss the following:
- Hard-coded Stub
- Configurable Stub
- Responder
- Saboteur
If you want, you can clone / download the examples used in this post on github.
The SUT and its collaborators
We want to write two unit tests for the getAddress
method in AddressService
. A unit test that covers the path in which formatter.formatAddressNotFound()
is returned and a test in that covers the path in which formatter.formatAddress(address)
is returned.
public class AddressService { private final AddressRepository repository; private final AddressFormatter formatter; public AddressService(AddressRepository repository, AddressFormatter formatter) { this.repository = repository; this.formatter = formatter; } public String getAddress(String postCode, int houseNumber, String suffix) { final Address address = repository.getAddress(postCode, houseNumber, suffix); if (address == null) { return formatter.formatAddressNotFound(); } return formatter.formatAddress(address); } }
The interface AddressFormatter
has two implementations HtmlFormatter
and JsonFormatter
which are already covered by unit tests. The interface AddressRepository
is not implemented and looks like this.
public interface AddressRepository { /** * @param postCode * @param houseNumber * @param suffix * @return address with or without suffix, {@code null} when no address can be retrieved */ Address getAddress(String postCode, int houseNumber, String suffix); }
For our unit test we’ll have to make a stub the getAddress
method in AddressRepository
. The AddressFormatter
does not necessarily need to be stubbed. But it does make sense to stub it, because we want the tests of getAddress
in AddressService
to fail for one reason and that reason is its own incorrect implementation. If we use a real AddressFormatter
such as the JsonFormatter
, we introduce a potential false negative. The unit test could fail because of an incorrect implementation of JsonFormatter
.
The Hard-coded Stub
One way make a real AddressRepository
is to create a class with a hard-coded response.
public class HardCodedStubAddressRepository implements AddressRepository { @Override public Address getAddress(String postCode, int houseNumber, String suffix) { City city = new City("Paradise City", new Province("FooBar", "FB")); Address validAddress = new Address("Test Street", 12, "c", city, "5555 TT"); return validAddress; } }
The same can be done for the AddressFormatter
.
public class HardCodedStubAddressFormatter implements AddressFormatter { @Override public String formatAddress(Address address) { return "Address"; } @Override public String formatAddressNotFound() { return "Address not found"; } }
These two hard-coded stubs function as a Responder. A Responder is a stub that is used to inject valid indirect inputs into the SUT. So now we are ready to write our first test.
@Test public void should_find_an_address_with_two_hardcoded_stubs() { // setup the hard-coded stubs AddressRepository stubRepository = new HardCodedStubAddressRepository(); AddressFormatter stubFormatter = new HardCodedStubAddressFormatter(); // instantiate the SUT with the hard-coded stubs AddressService service = new AddressService(stubRepository, stubFormatter); // verify SUT assertEquals("Address", service.getAddress("5555 TT", 12, "c")); }
This works fine for the Responder. However, we still need a Saboteur. A Saboteur is a stub that is used to inject invalid indirect inputs into the SUT. That invalid input is null
in this case. We can introduce an anonymous inner class.
// anonymous inner class that acts as a saboteur AddressRepository saboteurStubRepository = new AddressRepository() { @Override public Address getAddress(String postCode, int houseNumber, String suffix) { Address invalidAddress = null; return invalidAddress; } };
Our second test looks like this.
@Test public void should_find_no_address_with_two_hardcoded_stubs() { AddressFormatter stubFormatter = new HardCodedStubAddressFormatter(); // instantiate the SUT with the hard-coded anonymous inner class stub AddressService service = new AddressService(saboteurStubRepository, stubFormatter); // verify SUT assertEquals("Address not found", service.getAddress("TT5555", -12, "^")); }
The downside of a hard-coded stub is evident. Important setup details are partially outside the test method. Consequently, the test is hard to understand without a look at the separately maintained stubs.
The Configurable Stub
A configurable stub solves that problem. We simply add setters to our stub classes to make our tests control the responses.
public class ConfigurableStubAddressRepository implements AddressRepository { private Address address; public void setAddress(Address address) { this.address = address; } @Override public Address getAddress(String postCode, int houseNumber, String suffix) { return address; } } public class ConfigurableStubAddressFormatter implements AddressFormatter { private String formattedAddress; private String formattedAddressNotFound; public void setFormattedAddress(String formattedAddress) { this.formattedAddress = formattedAddress; } public void setFormattedAddressNotFound(String formattedAddressNotFound) { this.formattedAddressNotFound = formattedAddressNotFound; } @Override public String formatAddress(Address address) { return formattedAddress; } @Override public String formatAddressNotFound() { return formattedAddressNotFound; } }
The tests with configurable stubs are more self-contained and flexible.
public class ConfigurableAddressServiceTest { private ConfigurableStubAddressRepository stubRepository; private ConfigurableStubAddressFormatter stubFormatter; @Before public void setUp() { stubRepository = new ConfigurableStubAddressRepository(); stubFormatter = new ConfigurableStubAddressFormatter(); } @Test public void should_find_an_address_with_two_configurable_stubs() { City city = new City("Paradise City", new Province("FooBar", "FB")); Address validAddress = new Address("Test Street", 12, "c", city, "5555 TT"); // configure the stubRepository as responder stubRepository.setAddress(validAddress); // configure the stubFormatter as responder stubFormatter.setFormattedAddress("Address"); // instantiate the SUT with the configurable stubs AddressService service = new AddressService(stubRepository, stubFormatter); // verify SUT assertEquals("Address", service.getAddress("5555 TT", 12, "c")); } @Test public void should_find_no_address_with_two_configurable_stubs() { Address invalidAddress = null; // configure the stubRepository as saboteur stubRepository.setAddress(invalidAddress); // configure the stubFormatter as responder stubFormatter.setFormattedAddressNotFound("Address not found"); // instantiate the SUT with the configurable stubs AddressService service = new AddressService(stubRepository, stubFormatter); // verify SUT assertEquals("Address not found", service.getAddress("TT5555", -12, "^")); } }
This approach is better than the hard-coded stub. But is there an even quicker way? Rhetorically speaking, yes.
The Configurable stub with Mockito
We don’t need make our own stubs if we use Mockito.
public class AddressServiceTest { // the two stubs we want to use @Mock AddressFormatter stubFormatter; @Mock AddressRepository stubRepository; @Before public void setUp() { // use new stubs for each @Test MockitoAnnotations.initMocks(this); } @Test public void should_find_an_address_with_two_mockito_stubs() { City city = new City("Paradise City", new Province("FooBar", "FB")); Address validAddress = new Address("Test Street", 12, "c", city, "5555 TT"); // configure the stubRepository as responder when(stubRepository.getAddress("5555 TT", 12, "c")) .thenReturn(validAddress); // configure the stubFormatter as responder when(stubFormatter.formatAddress(validAddress)) .thenReturn("Address"); AddressService service = new AddressService(stubRepository, stubFormatter); // verify SUT assertEquals("Address", service.getAddress("5555 TT", 12, "c")); } @Test public void should_find_no_address_with_two_mockito_stubs() { Address invalidAddress = null; // configure the stubRepository as saboteur when(stubRepository.getAddress("TT5555", -12, "^")) .thenReturn(invalidAddress); // configure the stubFormatter as responder when(stubFormatter.formatAddressNotFound()) .thenReturn("Address not found"); AddressService service = new AddressService(stubRepository, stubFormatter); // verify SUT assertEquals("Address not found", service.getAddress("TT5555", -12, "^")); } }
With @Mock we declare our stubs and in the setUp
we instantiate them with initMocks. Configuring the stubs is self-explanatory. With when we tell the stub when to return something and with thenReturn
we tell it what to return.
The Stub in context
Using a stub can be very helpful for isolating our (unit) tests and helping them give specific and otherwise hard to simulate feedback. However, using only stubs can become disadvantageous if we never use ‘the real thing’. Units should also be tested for integration so that we know that internal contracts are still valid.
- Test Doubles: creating a Dummy Object using a mocking framework
- Test Doubles: creating a Test Spy
Pingback: Test Doubles: creating a Test Spy – test.right
Pingback: Test Doubles: creating a Mock – test.right