ArgumentCaptor in Mockito
0 CommentsLast Updated on February 3, 2021 by jt
ArgumentCaptor
in Mockito allows you to capture arguments passed to methods for further assertions. You can apply standard JUnit assertion methods, such as assertEquals()
, assertThat()
, and so on, to perform assertions on the captured arguments. In Mockito, you will find the ArgumentCaptor
class in the org. mockito
package
If you are new to mocking with Mockito, I suggest you go through my earlier post Mocking in Unit Tests with Mockito
In this post, I will explain how to create an ArgumentCaptor
, its important methods, and how to use them.
Dependency
To use Mockito, you’ll need to add the following dependency in your pom.xml.
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.23.4</version> <scope>test</scope> </dependency>
Sample Application
The sample application is a simple Spring Boot application. It has a Student
entity having id
and name
as its properties.
You can find the source code of the sample application here on Github.
The code of the Student
entity class is this.
Student.java
@Entity public class Student { @Id private int id; private String name; public Student() { } public Student(int id, String name) { Id = id; this.name = name; } public int getId() { return Id; } public void setId(int id) { Id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Student{ Id = " + Id + ", name = '" + name + '\'' + '}'; } }
The application also have a service implementation to save Student
entities and a Spring Data JPA repository interface.
The code of the StudentRepository
interface is this.
StudentRepository.java
package springframework.repository; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; import springframework.domain.Student; @Repository public interface StudentRepository extends CrudRepository<Student, Integer> { }
The code of the StudentService
interface is this.
StudentService.java
package springframework.service; import springframework.domain.Student; public interface StudentService { Student saveStudent(Student student); }
The implementation class is this.
StudentServiceImpl.java
package springframework.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import springframework.domain.Student; import springframework.repository.StudentRepository; @Service public class StudentServiceImpl implements StudentService { private StudentRepository studentRepository; @Autowired public StudentServiceImpl(StudentRepository studentRepository) { this.studentRepository = studentRepository; } @Override public Student saveStudent(Student student) { return studentRepository.save(student); } }
Setting up the Unit Test
I have created a test class ArgumentCaptorTest.java
. The code is this
@RunWith(MockitoJUnitRunner.class) public class ArgumentCaptorTest { @Captor private ArgumentCaptor<Student> captor; @Mock private StudentRepository studentRepository; @InjectMocks private StudentServiceImpl studentService;
In the code above, Line 1 runs the test class with @RunWith(MockitoJUnitRunner.class)
to make Mockito detect ArgumentCaptor
, which we will declare next.
Line 2 – Line 3 creates an ArgumentCaptor
of type Student
and annotates it with @Captor
to store captured argument.
Line 7 – Line 11 uses the @Mock
annotation to mock StudentRepository
, which is then automatically injected into our StudentServiceImpl
with the @InjectMocks
annotation.
Test Case to show ArgumentCapture Usage
Now, I will show the usage of ArgumentCaptor
. This is the code.
@Test public void shouldCapture() { Student student1 = new Student(1, "Harry"); studentService.saveStudent(student1); Mockito.verify(studentRepository).save(captor.capture()); assertEquals("Harry", captor.getValue().getName()); assertEquals(1,captor.getValue().getId()); }
To capture the method arguments, you need to use the capture()
method of ArgumentCaptor
. You should call it during the verification phase of the test.
In the code provided above, Line 4 – Line 5 creates and saves a Student
object student1
.
Line 7 calls Mockito.verify()
to verify if the save()
method of the mocked StudentRepository
has been called. Then the call to captor.capture()
captures the method argument passed to the mock method.
Line 9 – Line 10 perform assertions by calling the getValue()
method of ArgumentCaptor
to get the captured value of the argument.
Note: If the verified methods are called multiple times, then the getValue()
method will return the latest captured value.
Now, let’s run our test.
This figure shows that the test case has passed successfully.
Multiple Captures using ArgumentCaptor
In the previous test case, we captured only one value, since there was only one verify method. To capture multiple argument values, ArgumentCaptor
provides the getAllValues()
method.
The test code is this.
@Test public void shouldCaptureMultipleTimes() { Student student1 = new Student(1, "Harry"); Student student2 = new Student(2, "Tae"); Student student3 = new Student(3, "Louis"); studentService.saveStudent(student1); studentService.saveStudent(student2); studentService.saveStudent(student3); Mockito.verify(studentRepository,Mockito.times(3)).save(captor.capture()); List studentList = captor.getAllValues(); assertEquals("Harry", studentList.get(0).getName()); assertEquals("Tae", studentList.get(1).getName()); assertEquals("Louis", studentList.get(2).getName()); }
In the code provided above, Line 4 – Line 10 creates and saves three Student
objects named student1
, student2
, and student3
.
Then, Line 12 calls the Mockito.verify()
method to check that the StudentRepository.save()
is called thrice. Additionally, the call to captor.capture()
captures the three values.
Line 14 obtains the list of captured values by the getValues()
method. If you call verify methods multiple times, the getAllValue()
method will return the merged list of all the values from all invocations.
Finally, the assertEquals()
methods perform assertions on the captured arguments.
The output of the test is this.
The ArgumentCaptor.forClass() Method
Till now we have used the @Captor
annotation to instantiate ArgumentCaptor
in our test. Alternatively, you can use the ArgumentCaptor.forClass()
method for the same purpose.
The test code is this.
ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Student.class); @Test public void shouldCaptureManually() { Student student1 = new Student(1, "Harry"); studentService.saveStudent(student1); Mockito.verify(studentRepository).save(argumentCaptor.capture()); Student captured = argumentCaptor.getValue(); assertEquals("Harry", captured.getName()); }
On running the test, you should see the test passing successfully.
Summary
I have seen test code using ArgumentCaptor with stubbing, an approach that I don’t advocate.
Let’s say, you use ArgumentCaptor
during stubbing with Mockito.when
, like this.
UserRegistration userRegistration = new UserRegistration ("Jammie", "[email protected]", 23); Mockito.when(userRegistrationService.registerUser(registrationCaptor.capture())).thenReturn(userRegistration); assertTrue(userService.register(userRegistration)); assertEquals(userRegistration, registrationCaptor.getValue());
This code decreases readability as compared to the conventional way of using Mockito.eq(userRegistration)
. Additionally, the call to registrationCaptor.capture()
during stubbing lack clarity of its intent. Also, you end up with an extra assertion – the final assertEuals()
, which you wouldn’t have required with the conventional Mockito.eq(userRegistration)
.
In addition, suppose userService.register()
doesn’t call userRegistrationService.register()
in the actual code. In this scenario, you will get this exception
org.mockito.exceptions.base.MockitoException: No argument value was captured!
This exception message is confusing and can trip you to believing that you have issues in your test. However, the issue is in the code you are testing and not in your test.
So the best practice is to use ArgumentCaptor
the way it is designed for – during verification.
You can find the source code of this post on Github.
For in-depth knowledge on Testing in Spring Boot Applications check my Udemy Best Seller Course Testing Spring Boot: Beginner to Guru.