Mockito: Simplifying Tests with Argument Matchers
1. Outline
- Short Refresh: Mockito
- Argument Matchers
- Real Example
- Takeaways
2. Short Refresh: Mockito
Mockito is a popular Java framework used for unit testing that allows developers to create mock objects to simulate the behaviour of real objects. This enables code testing in isolation by faking dependencies, defining their behaviour, and verifying interactions. This ensures that tests are focused, reliable, and do not rely on external systems like databases or network services.
3. Argument Matchers
Argument matchers are tools that help us specify general and flexible conditions/values for the arguments of a method call when we are writing tests. Instead of specifying the exact value an argument must have, we can specify conditions it should meet (e.g., any value, a specific type). Using matchers can make our tests more readable by focusing on the conditions that matter rather than on specific values.
Let’s say we have a method, someMethod(),
and it takes a String
argument like below:
Mockito.when(mockedObject.someMethod("example")).thenReturn(someValue);
Without using argument matchers, we need to specify the exact argument value that someMethod()
is expected to be called with. This means we must know the exact argument in advance. If someMethod()
is called with anything other than "example", the mock won't return someValue
. This makes the test less flexible because it will only pass if the method is called with that exact argument.
If we make use of argument matchers the code will change like below:
Mockito.when(mockedObject.someMethod(anyString())).thenReturn(someValue);
Here, anyString()
is an argument matcher that matches any string. In this case, the mock will return somevalue
whenever someMethod
is called.
Further, we can see a list of some of the most commonly used argument matchers in Mockito:
any()
matches any objectany(Class<T> type)
matches any object of the specified typeanyInt()
matches anyint
valueanyString()
matches anyString
valueeq(T value)
matches if the argument equals the given valueisNull()
matches if the argument isnull
isNotNull()
matches if the argument is notnull
contains(String substring)
matches if the argument contains the substring
4. Real Example
Let’s take an example to see how this works for real applications. We’ll make some unit tests for a Spring Boot app, written in Java and built with Gradle.
Prerequisites:
- Project: Gradle
- Language: Java 17
- Framework: Spring Boot 3.3.0
- Dependencies: Spring Web, Lombok, Mockito and JUnit
The app is called bookApp and it only has one controller named BookController
.
package com.dep.bookApp.controllers;
import java.util.List;
import com.dep.bookApp.models.Book;
import com.dep.bookApp.services.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/books")
public class BookController {
private final BookService bookService;
@Autowired
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping("/")
public ResponseEntity<List<Book>> findBooks(@RequestParam String author, @RequestParam String genre) {
List<Book> books = bookService.findBooks(author, genre);
if (books.isEmpty()) {
return ResponseEntity.noContent().build();
} else {
return ResponseEntity.ok(books);
}
}
}
We can see that it exposes a GET
resource that finds books by author
and genre
.
OK, now let’s take a look at the BookControllerTest
.
package com.dep.bookApp.controllers;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.List;
import com.dep.bookApp.models.Book;
import com.dep.bookApp.services.BookService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.ResponseEntity;
@ExtendWith(MockitoExtension.class)
public class BookControllerTest {
@InjectMocks
private BookController bookController;
@Mock
private BookService bookService;
@Test
public void findBooks_ReturnsNoContent() {
when(bookService.findBooks(anyString(), anyString())).thenReturn( Collections.emptyList());
ResponseEntity<List<Book>> response1 = bookController.findBooks("Author1", "Comedy");
ResponseEntity<List<Book>> response2 = bookController.findBooks("Author2", "Drama");
assertEquals(204, response1.getStatusCode().value());
assertEquals(204, response2.getStatusCode().value());
}
@Test
public void findBooks_ReturnsListOfBooks() {
Book book = new Book();
book.setTitle("Title");
book.setAuthor("Author");
book.setGenre("Comedy");
when(bookService.findBooks( anyString(), anyString())).thenReturn( List.of( book ) );
ResponseEntity<List<Book>> response1 = bookController.findBooks( "Author", "Comedy");
assertEquals(200, response1.getStatusCode().value());
assertEquals(1, response1.getBody().size());
assertEquals("Title", response1.getBody().get(0).getTitle());
assertEquals("Author", response1.getBody().get(0).getAuthor());
assertEquals("Comedy", response1.getBody().get(0).getGenre());
ResponseEntity<List<Book>> response2 = bookController.findBooks( "Test", "Drama");
assertEquals(200, response2.getStatusCode().value());
assertEquals(1, response2.getBody().size());
assertEquals("Title", response2.getBody().get(0).getTitle());
assertEquals("Author", response2.getBody().get(0).getAuthor());
assertEquals("Comedy", response2.getBody().get(0).getGenre());
}
}
As we can see, it doesn’t matter with which String
values we call findBook()
method, it will always return an empty list for the first unit test or the book
object for the second one. More than this, we don’t have to set up multiple when
statements for different combinations of author and genre. One when
statement covers all possible combinations.
5. Takeaways
- Flexibility: Allows general conditions for arguments, making tests more adaptable
- Simplified Setup: Reduces the number of
when
statements needed, easing test setup - Readability: Makes test intent clearer with expressive matchers
- Dynamic Data: Handles unpredictable inputs robustly
- Reduced Redundancy: Avoids repetitive stubs for different argument values
You can find the entire code in my repository on GitLab.