Hopp til hovedinnhold

Tips og triks / 6 minutter /

Utilizing the power of Mockito with Quarkus

How often have you wanted to write an integration test for a piece of code that communicates with a lot of external parts, like APIs?

This is a case we as developers often face and something we need to handle. When testing a piece of code that communicates with an API, we can connect to the actual API during the test, which makes your test vulnerable to downtime or changes in the API. You could also set up your own temporary API that exists solely for testing purposes. This is a viable solution, but takes some effort. A third solution, which is the topic of this blogpost, is mocking!

When testing a piece of code we want to isolate its behaviour. To achieve this goal it can be helpful to replace other parts of our code with simulations of their behaviour. We could then simulate a class responsible for sending a POST-request to an external API, instead of having to communicate with an actual API. This way we are able to test the adjacent logic, like the code verifying that the data about to be sent is valid.

Depending on what tests we are running we might want our mocks to behave differently. This tutorial will go through some different approaches to mocking using Quarkus and Mockito, and talk about some pros and cons with each of them. This tutorial is primarily aimed at developers who are familiar with writing tests in Java with Quarkus, but are new to mocking.

The tests will focus on testing an EmployeeService class, testing its functionality while trying to work around its dependencies to the injected REST-clients. Feel free to clone the example repository and check out the code for yourself. Information about the needed dependencies can also be found there. Having cloned the repository all you need is Java and Maven installed to run the tests with "mvn verify".

The Mock annotation

Quarkus allows us to use the @Mock annotation, which will override a Bean. For example if we have a rest-client that interacts with an external API to fetch employees like this one:

1@RegisterRestClient(configKey = "api")
2public interface EmployeeApi {
3
4    @GET
5    @Path("employee/{employeeNumber}")
6    Employee getEmployee(@PathParam("employeeNumber") String employeeNumber);
7
8    @POST
9    @Path("employee")
10    Employee addEmployee(Employee employee);
11
12}

We can create a mock implementation, that will override the rest client. It could look something like this:

1@Mock
2@ApplicationScoped
3@RestClient
4public class EmployeeApiMock implements EmployeeApi {
5
6    @Override
7    public Employee getEmployee(String employeeNr) {
8        if(employeeNr.equals("404")){
9            throw new NotFoundException("Could not find employee");
10        }
11        return new Employee("Test", employeeNr);
12    }
13
14    @Override
15    public Employee addEmployee(Employee employee) {
16        return employee;
17    }
18
19}

When using the mock annotation, your mock implementation wil override the rest client in all the tests, unless they specifically inject their own mocked version. If you want your mock to behave differently based on its input parameters, you would have to implement logic for that in the mock, which provides more complexity to the test, and most importantly that logic affects your test, but is not located in the test itself, making it harder for someone reading your test to understand what is going on.

Now if we have a method in an EmployeeService class that we want to test:

1@ApplicationScoped
2public class EmployeeService {
3
4    @Inject
5    @RestClient
6    EmployeeApi employeeApi;
7
8    public Optional<Employee> getEmployee(String employeeNumber) {
9        try {
10            return Optional.of(employeeApi.getEmployee(employeeNumber));
11        } catch (Exception e) {
12            return Optional.empty();
13        }
14    }
15    
16}

We could write some tests like this, and it would test that the NotFoundException given by employeeNumber "404" would result in an empty result. But why the employee number of "404" should differ from the "1" might not be immediately clear for readers, as this information is contained in the EmployeeApiMock class. What we are really trying to test here is that an exception results in an Optional.empty() returned.

1@QuarkusTest
2class GetEmployeeUsingMockClassTest {
3
4    @Inject
5    EmployeeService employeeService;
6
7    @Test
8    void testThatNotFoundExceptionGivesEmptyResponse(){
9        var result = employeeService.getEmployee("404");
10        Assertions.assertFalse(result.isPresent());
11    }
12
13    @Test
14    void testThatEmployeeIsReturned(){
15        var result = employeeService.getEmployee("1");
16        Assertions.assertTrue(result.isPresent());
17    }
18}

On the other hand, if we want the mock-implementation to behave the same way globally across all of our tests a Mock-class (also called a fake) is a simple way to remove the need for an external dependency. Setting up a Mock-class can require few lines of code and can be a clean solution if the tests are not directly tied to the Mock-implementation itself, like the example above. If we however want a more complex mock, it could become a little more bothersome to write a Mock-class that serves the need of all of our tests. In these cases it could be easier to set up a specific Mock for a single test, which leads into the next subject.

@InjectMock

Quarkus allows us to define the mock implementation per test. Instead of creating a separate mock implementation class, we define the mock inside the test class. Quarkus documentation refers to the above method as the old approach, while this method is described as the new approach. Additionally, we use argument matchers to define logic based on input parameters. Our mock class and test can both be replaced by the following test.

1@QuarkusTest
2public class GetEmployeeUsingInjectMockTest {
3
4    @InjectMock
5    @RestClient
6    EmployeeApi employeeApi;
7
8    @Inject
9    EmployeeService employeeService;
10
11    @BeforeEach
12    public void setup(){
13        when(employeeApi.getEmployee(anyString())).thenAnswer(invocation -> new Employee("Test", invocation.getArgument(0)));
14        when(employeeApi.getEmployee("404")).thenThrow(new NotFoundException("Could not find Employee"));
15    }
16
17    @Test
18    void testThat404GivesNull(){
19        var result = employeeService.getEmployee("404");
20        Assertions.assertFalse(result.isPresent());
21    }
22
23    @Test
24    void testThatEmployeeIsReturned(){
25        var result = employeeService.getEmployee("1");
26        Assertions.assertTrue(result.isPresent());
27    }
28
29}

This setup provides the same result, but with the mock setup inside the test class itself. A big benefit of this is that whoever reads the tests can easily see what data is mocked. Argument matchers can be used to customize the mock response based on the parameters, like we do in the test above by returning an employee with the same employeeNumber used in the query. More information about the powers of argument matchers can be found here: https://www.baeldung.com/mockito-argument-matchers

Verify

Sometimes we might have a piece of code that executes several other methods, like sending post requests to an API, without directly affecting the output of the method you are testing. Mockito's verify function comes to the rescue. A small note: When writing tests we usually want to try to not couple our tests too tightly to the internal logic of our system. If we make a change to the internal logic, we end up breaking the tests, even if the system still does what we want it do. Verify takes us somewhat into this territory, so make sure to only use it when necessary.

When we create new employees in our system, we need to create tickets to notify IT or administrators that a new employee has started. This ticket is created through a post request to an external API. The EmployeeService would have a method like this:

1public Employee addEmployee(Employee employee) {
2        if(isValidEmployee(employee)){
3            ticketApi.createTicket(employee);
4            return employeeApi.addEmployee(employee);
5        }
6        return null;
7    }
8    
9public boolean isValidEmployee(Employee employee){
10        return employee.name() != null && employee.employeeNumber() != null;
11    }

We want to test that whenever we add a valid employee, we also create a ticket by calling the createTicket method of our ticketApi rest-client. This can be achieved by using Mockito's verify-method like in the following test:

1@QuarkusTest
2public class AddEmployeeUsingVerifyTest {
3
4    @InjectMock
5    @RestClient
6    TicketApi ticketApi;
7
8    @Inject
9    EmployeeService employeeService;
10
11    @Test
12    void testAddEmployeeCreatesTicket() {
13        Employee employee = new Employee("Mr. Test", "123");
14
15        employeeService.addEmployee(employee);
16
17        Mockito.verify(ticketApi, Mockito.times(1)).createTicket(employee);
18    }
19
20    @Test
21    void testAddInvalidEmployeeDoesNotCreateTicket() {
22        Employee employee = new Employee(null, "123");
23
24        employeeService.addEmployee(employee);
25
26        Mockito.verify(ticketApi, Mockito.times(0)).createTicket(employee);
27    }
28
29}

@InjectSpy

Now what if we want to mock a specific method in an injected class, but use the rest as is. This is where @InjectSpy comes in handy. Imagine that in our EmployeeService class, we want to test its functionality, but mock the isValidEmployee method. We could do so like this:

1@QuarkusTest
2public class AddEmployeeUsingInjectSpyTest {
3
4    @InjectSpy
5    EmployeeService employeeService;
6
7    @Test
8    void testAddEmployee() {
9        Employee employee = new Employee(null, "123");
10        when(employeeService.isValidEmployee(employee)).thenReturn(true);
11
12        var result = employeeService.addEmployee(employee);
13
14        Assertions.assertNotNull(result);
15        Mockito.verify(employeeService, Mockito.times(1)).isValidEmployee(employee);
16    }
17
18    @Test
19    void testAddEmployeeDoesNotCallAPIIfInvalid() {
20        Employee employee = new Employee(null, "123");
21        when(employeeService.isValidEmployee(employee)).thenReturn(false);
22
23        var result = employeeService.addEmployee(employee);
24
25        Assertions.assertNull(result);
26        Mockito.verify(employeeService, Mockito.times(1)).isValidEmployee(employee);
27    }
28    
29}
30

The functionality of the injected EmployeeService is unchanged, except for the isValidEmployee method, that we override with the mock implementation. The injected spy is also compatible with verify, as a bonus. InjectSpy can be practical if there is a specific method that we do not care about in our test, but we want to test the rest of the injected class as is. You probably should not mock your validation methods, but the example above simply illustrates how to use an injected spy.

mockStatic

Mockito also allows us to mock static methods. In the example repo we utilize a class with a static method to verify if a Quarkus security identity contains a specific role. This can be hard to test for, so it could be easier to mock this method. If we extend the getEmployee method to also check the user's credential like this:

1public Employee getEmployeeAuth(String id) {
2    if(UserAccess.isAdmin(securityIdentity)){
3        return employeeApi.getEmployee(id);
4    }
5    else throw new ForbiddenException("You don't have access!");
6}

With the UserAccess class looking like this:

1public class UserAccess {
2
3    public static boolean isAdmin(SecurityIdentity securityIdentity){
4        return securityIdentity.hasRole("EmployeeAdmin");
5    }
6}

We can mock the call to isAdmin with Mockito's mockStatic function, demonstrated in this test:

1@QuarkusTest
2public class GetEmployeeUsingMockStaticTest {
3
4
5    @Inject
6    EmployeeService employeeService;
7
8    @InjectMock
9    @RestClient
10    EmployeeApi employeeApi;
11
12    @BeforeEach
13    public void setup(){
14        when(employeeApi.getEmployee(anyString())).thenAnswer(invocation -> new Employee("Test", invocation.getArgument(0)));
15    }
16
17    @Test
18    void testGetEmployeeWhenAdmin(){
19        try(var userAccessMock = Mockito.mockStatic(UserAccess.class)) {
20            userAccessMock.when(() -> UserAccess.IsAdmin(any())).thenReturn(true);
21            var result = employeeService.getEmployeeAuth("1");
22            Assertions.assertEquals("Test", result.name());
23        }
24    }
25
26    @Test
27    void testGetEmployeeWhenNotAdmin(){
28        try(var userAccessMock = Mockito.mockStatic(UserAccess.class)) {
29            userAccessMock.when(() -> UserAccess.IsAdmin(any())).thenReturn(false);
30            Assertions.assertThrows(ForbiddenException.class, () -> employeeService.getEmployeeAuth("1"));
31        }
32    }
33
34}
35

Notice how we set up the mocking of the employeeApi in the same way as before. Mockstatic introduces some more lines of code to your test, so consider if what you are testing really needs to be static. Maybe the function could be refactored and tested in another way. In any case, mockStatic is there if there is a need to test static functionality.

Conclusion

To wrap up, Mockito allows us to focus on system behaviour instead of dependencies. This tutorial has highlighted some of Mockito's functionalities that allows for creating efficient tests. Remember, the ultimate goal is a reliable codebase that's easy to update and maintain. Keep experimenting with Mockito and Quarkus to power up your tests. Happy testing!

This article was originally posted on Github. You can read it here