Mockito Spy (@Spy)
A spy,unlike a mock is used to monitor the actual call to the functioning of a method under test.When a mocked instance’s method is invoked,it does nothing and we can control its returned result or even throw exceptions.With a spied object,the actual method is called .We can however mock its functionality the same way we do with a mock.
Lets see @Spy with an example.
Spy Example
We shall write a test for the TeacherService class, createTeacher() method.
The TeacherService source code is as below.
package com.school.relationships.services;
import com.school.relationships.entities.Teacher;
import com.school.relationships.models.TeacherModel;
import com.school.relationships.repositories.TeacherRepository;
import com.school.relationships.util.FormattingUtils;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class TeacherService {
@Autowired
private TeacherRepository teacherRepository;
@Autowired
private FormattingUtils formattingUtils;
public Teacher createTeacher(TeacherModel model) {
try {
Teacher teacher = new Teacher();
teacher.setFullName(model.getTeacherName());
teacher.setTeacherNo(formattingUtils.formatTeacherNumber(model.getTeacherNumber()));
log.info("Created teacher name={},teacher no={}", teacher.getFullName(), teacher.getTeacherNo());
return teacherRepository.save(teacher);
} catch (Exception e) {
log.error("Error creating teacher > ", e);
throw e;
}
}
public Optional<Teacher> findTeacherById(Integer idteacher) {
return teacherRepository.findById(idteacher);
}
public Optional<Teacher> findTeacherAndSubjectsTaught(Integer idteacher) {
return teacherRepository.findTeacherAndSubjectsTaught(idteacher);
}
}
The class has two (2) dependencies TeacherRepository and FormattingUtils.
We are going to mock TeacherRepository’s save() method which persists our Teacher entities and Spy on FormattingUtils which is a utility class for sanitizing and formatting strings on some fields on entities before persisting them in the database.For our TeacherService method,we use the FormattingUtils’s formatTeacherNumber() method to make our passed in teacher number start with ‘TR-‘ and also capitalize the whole string.
By Spying on FormattingUtils ,we can see that an actual call to its formatTeacherNumber is made by observing the formatted teacher number on the logs .
Our unit test case is as below
package com.school.relationships;
import com.school.relationships.entities.Teacher;
import com.school.relationships.models.TeacherModel;
import com.school.relationships.repositories.TeacherRepository;
import com.school.relationships.services.TeacherService;
import com.school.relationships.util.FormattingUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
@Slf4j
public class TeacherServiceTests {
@Mock
private TeacherRepository teacherRepository;
@Spy
private FormattingUtils formattingUtils;
@InjectMocks
private TeacherService teacherService;
@Test
public void testCreateTeacherSuccessful() {
log.info("Started testing method createTeacherSuccessful");
Mockito.when(teacherRepository.save(Mockito.any(Teacher.class))).thenReturn(Mockito.mock(Teacher.class));
TeacherModel model = new TeacherModel();
model.setTeacherName("Mr. John");
model.setTeacherNumber("123");
teacherService.createTeacher(model);
log.info("Finished testing method createTeacherSuccessful");
}
}
Once we build our project to run the unit tests,
We see below log line ‘20:40:44.609 [main] INFO com.school.relationships.services.TeacherService - Created teacher name=Mr. John,teacher no=TR-123’
We can see that an actual call to the formatTeacherNumber(String trNo) method in FormattingUtils class was called.
Stubbing a spy
We can also stub a spy by modifying its execution to return a different result.
Let’s do this by writing the below test case.
@Test
public void testCreateTeacherSuccessfulWithFormattingUtilsModification() {
log.info("Started testing method testCreateTeacherSuccessfulWithFormattingUtilsModification");
Mockito.when(teacherRepository.save(Mockito.any(Teacher.class))).thenReturn(Mockito.mock(Teacher.class));
Mockito.when(formattingUtils.formatTeacherNumber(Mockito.anyString())).thenAnswer((InvocationOnMock iom) -> {
String trNo = iom.getArgument(0, String.class);
return new StringBuilder().append("tr-").append(trNo.toLowerCase()).toString();
});
TeacherModel model = new TeacherModel();
model.setTeacherName("Mr. John");
model.setTeacherNumber("123");
teacherService.createTeacher(model);
log.info("Finished testing method testCreateTeacherSuccessfulWithFormattingUtilsModification");
}
We override the functionality of the formatTeacherNumber method so that we append lower case “tr-” and the lowercase teacher number together vs the original version in which the result was in uppercase.
On building and running the project,we get below log extract for the testcase.
The log line tagged latest is from the test case with stubbed spy while the one tagged original is from the test case with spy ,i.e. the original spy method called.
Tip on scenarios to use spy
When writing tests, not all functionality needs to be mocked. For some scenarios, we actually want to see the actual code executing .This can be helpful so that the returned result can be used in writing meaningful assertions.In this situtations we use Spy .
The code for this project at this stage is available at Github.