STUB, MOCK, SPY
In computer programming and computing, programmers use a technique called automated unit testing to improve software quality. In order to increase the isolation and independence of a unit test, so-called test doubles are used that actually “fake” dependent objects, but are not them. An example of this type of test double can be an object pretending to read information from a database needed for tests of business logic in our code.
Generally, we can distinguish 5 types of test doubles:
• Dummy – an object that is never really used, it is only needed to complete the list of method parameters;
• Fake (fake / imitation) – used as a simpler implementation, incl. using an in-memory database in tests instead of accessing the actual database.
• Stub (Substitute) – Stubs are objects that store predefined data and use it to provide answers during tests. In other words, the stub is an object that resembles a real object with a minimum number of methods needed for the test. Stubs are used when we don’t want to use objects, which would give an answer with real data. It is referred to as the lightest and most static version of test double. A stub is created by extending a real object or interface and implementing the necessary methods as required by the test scenario. The stub is state-oriented and its behavior is static, i.e. known at compile-time.
• Mock (taunt / mockery) – an object that is instructed how to interact with the test: whether it should return specific values, or maybe he should raise an exception. Then, in the verification phase, we check whether the mock interacted with the expected objects by the appropriate length of time or the appropriate number of times. The main function of using mocks is that it gives you full control over the behavior of mock objects. They are mainly built using a library or mocking framework such as Mockito, JMock, and EasyMock. It is used to test a large suite of tests where the sections are not sufficient. Mock is behavior oriented and can be modified at runtime – it is dynamic.
• Spy – Spies are known to be partially mock objects. This means that the spy creates a partial object or a semi-mock real object through spying on real methods. When spying, the real object remains unchanged and we just follow some specific methods. In other words, we take an existing (real) object and replace it or spy on only some of its methods. Spies are useful when we have a huge class full of methods and want to mock certain methods. In this scenario, we should be using spies rather than mockery and substitutes. The spy calls the actual methods, if the methods are not substitutes.
In this article, I would like to pay special attention to the following test doubles: Stub, Mock, and Spy.
Let’s analyze the example below – When a new customer appears with a booking date containing the year 2022, we want to accept the booking, otherwise, we will not accept bookings as they cannot be fulfilled. First, let’s prepare the base classes: Client, Reservation, ReservationService and the ReservationRepository interface:
public class Client {
private String name;
private String date;
public Client(){}
public Client(String name, String date) {
this.name = name;
this.date = date;
}
public String getName() {
return name;
}
public String getDate(){
return date;
}
public String getNameAndDate() {
return getName() + " " + getDate();
}
}
public class Reservation {
private boolean active;
public Reservation(Client clientReservation) {
if(clientReservation.getDate().contains("2022")){
activate();
} else {
this.active = false;
}
}
public void activate(){
this.active = true;
}
public boolean isValid(){
return this.active;
}
}
public interface ReservationRepository {
List<Reservation> getAllReservations();
}
public class ReservationService {
private final ReservationRepository reservationRepository;
public ReservationService (ReservationRepository reservationRepository){
this.reservationRepository = reservationRepository;
}
List<Reservation> getAllActiveReservations(){
return reservationRepository.getAllReservations().stream()
.filter(Reservation::isValid)
.collect(Collectors.toList());
}
}
STUB
First, let’s consider how we would test our code using a stub. If we want to test the method that returns all accepted reservations, we will need data. Since we do not have access to any data, we can use a Stub type double that will provide sample data for verification:
public class ReservationRepositoryStub implements ReservationRepository{
@Override
public List<Reservation> getAllReservations() {
Client client1 = new Client("Adam Bodziak", "2022-12-12");
Reservation reservation1 = new Reservation(client1);
Client client2 = new Client("Kasia Lubi", "2022-12-31");
Reservation reservation2 = new Reservation(client2);
Client client3 = new Client("Marysia Motyka", "2022-12-25");
Reservation reservation3 = new Reservation(client3);
return Arrays.asList(reservation1, reservation2, reservation3);
}
}
Thanks to this, we can create a test class using the above class ReservationRepositoryStub: The test class will use assertions to compare statically set values against the expected result – in our case it is the size of the list.
public class ReservationServiceStubTest {
@Test
void getAllActiveReservations(){
//given
ReservationRepository reservationRepositoryStub = new ReservationRepositoryStub();
ReservationService reservationService = new ReservationService(reservationRepositoryStub);
//when
List<Reservation> reservationsList = reservationService.getAllActiveReservations();
//then
assertThat(reservationsList, hasSize(3));
}
}
Here, we had a pre-prepared Stub class that used the interface method to set up the data that was then used during the test.
MOCK
Now, let’s look at how you can accomplish the same task with a Mock test double from the Mockito library:
public class ReservationServiceMockTest {
@Test
void getAllActiveReservations() {
//given
List<Reservation> reservations = prepareReservationsData();
ReservationRepository reservationRepositoryMock = mock(ReservationRepository.class);
ReservationService reservationService = new ReservationService(reservationRepositoryMock);
given(reservationRepositoryMock.getAllReservations()).willReturn(reservations);
//when
List<Reservation> reservationsList = reservationService.getAllActiveReservations();
//then
assertThat(reservationsList, hasSize(2));
}
private List<Reservation> prepareReservationsData(){
Client client1 = new Client("Adam Budzik", "2022-12-12");
Reservation reservation1 = new Reservation(client1);
Client client2 = new Client("Kasia Lubi", "2021-12-31");
Reservation reservation2 = new Reservation(client2);
Client client3 = new Client("Marysia Motyka", "2022-12-25");
Reservation reservation3 = new Reservation(client3);
return Arrays.asList(reservation1, reservation2, reservation3);
}
}
The Mockito library allows you to mock classes similar to interfaces and allows you to create your own matchers. For this purpose, for example, the Hamcrest library can also be used. Compared to stubs, mocks can be created dynamically while the code is running and provide greater flexibility.
SPY
Finally, I would like to show how we can use a double type Spy, for example, on the mentioned earlier Client class:
public class ReservationServiceSpyTest {
@Test
void spyTest() {
//given
Client client = spy(Client.class);
given(client.getDate()).willReturn("2022-01-11");
given(client.getName()).willReturn("Katarzyna Gruszka");
//when
String result = client.getNameAndDate();
//then
assertThat(result, equalTo("Katarzyna Gruszka 2022-01-11"));
}
}
After creating an object of the Client class and using the spy () method on this class, we can directly refer to individual methods of this class, to force their specific behavior. Using the willReturn () method, we set what specific methods of this class would return, then by calling method from the Client class (getNameAndDate()), we can check if the returned value matches with the previously set data. We see that in this case we are caling the real methods of the class we are spying on.
Summary
I hope that the above example of using Stub, Mock and Spy doubles is understandable and emphasizes their main characteristics. Being able to write tests can prove very useful. Although from a technical point of view, the test double can be written with using a framework (though not everyone and not always), readability and management are sometimes better in favor of a self-written class. Let’s not forget at the same time that we may also encounter other types of test doubles.