1. Introduction
Mocking is a testing technique that replaces real components with objects that have predefined behavior. This allows developers to isolate and test specific components without relying on dependencies. Mocks are objects with predefined answers to method calls, which also have expectations for executions.
In this tutorial, we’ll see how we can test a user role-based authentication service using Mockito based on different roles with the help of the Answer API provided by Mockito.
2. Maven Dependencies
Before heading into this article, adding the Mockito dependency is essential.
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.14.2</version>
<scope>test</scope>
</dependency>
Let’s add the JUnit 5 dependency, as we’ll need it for some parts of our unit testing.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>
3. Introduction to Answer API
The Answer API allows us to customize the behavior of mocked methods by specifying what they should return when invoked. This will be useful when we want the mock to provide different responses based on the input parameters. Let us explore a few more topics to build up to our topic with a clearer understanding of concepts.
The Answer API in Mockito intercepts method calls on a mock object and redirects those calls to custom-defined behavior. This process involves internal steps that allow Mockito to simulate complex behaviors without modifying the underlying code. Let’s explore this in detail, from mock creation to the method invocation interception.
3.1. Mock Creation and Stubbing with thenAnswer()
Mock creation in Mockito starts with the mock() method, which generates a proxy of the target class using CGLib or Reflection API. This proxy is then registered internally, enabling Mockito to manage its lifecycle.
After creating the mock, we proceed to method stubbing, defining specific behaviors for methods. Mockito intercepts this call, identifies the target method and arguments, and uses the thenAnswer() method to set up a custom response. The thenAnswer() method takes an interface implementation, allowing us to specify custom behavior:
// Mocking an OrderService class
OrderService orderService = mock(OrderService.class);
// Stubbing processOrder method
when(orderService.processOrder(anyString())).thenAnswer(invocation -> {
String orderId = invocation.getArgument(0);
return "Order " + orderId + " processed successfully";
});
// Using the stubbed method
String result = orderService.processOrder("12345");
System.out.println(result); // Output: Order 12345 processed successfully
Here, processOrder() is stubbed to return a message with the order ID. When called, Mockito intercepts and applies the custom Answer logic.
3.2. Method Invocation Interception
Understanding how Mockito’s Answer API works is essential for setting up flexible behavior in tests. Let’s break down the internal process that occurs when a method is called on a mock during test execution:
- When a method is called on the mock, the call is redirected to Mockito’s internal handling mechanism through the proxy instance.
- Mockito checks if a behavior has been registered for that method. It uses the method signature to find the appropriate Answer implementation.
- If an Answer implementation is found, then the information about the arguments passed to the method, method signature, and the reference to the mock object is stored in an instance of the InvocationOnMock class.
- Using InvocationOnMock, we can access method arguments with getArgument(int index) to control the method’s behavior dynamically
This internal process enables the Answer API to respond dynamically based on context. Consider a content management system where user permissions vary by role. We can use the Answer API to simulate authorization dynamically, depending on the user’s role and requested action. Let’s see how we’ll implement this in the following sections.
4. Creating Models of User and Action
Since we’ll use a content management system as an example, we’ll have four roles: Admin, Editor, Viewer, and Guest. These roles will serve as basic authorization for different CRUD operations. An Admin can perform all actions, an Editor can create, read, and update, a Viewer can only read content, and a Guest has no access to any actions. Let us start by creating a User class:
public class CmsUser {
private String username;
private String role;
public CmsUser(String username, String role) {
this.username = username;
this.role = role;
}
public String getRole() {
return this.role;
}
}
Now, let’s define an enumeration to capture the possible CRUD operations using the ActionEnum class:
public enum ActionEnum {
CREATE, READ, UPDATE, DELETE;
}
With ActionEnum defined, we’re ready to start with the service layer. Let’s begin by defining the AuthorizationService interface. This interface will contain a method to check whether a user can perform a specific CRUD action:
public interface AuthorizationService {
boolean authorize(CmsUser user, ActionEnum actionEnum);
}
This method will return whether a given CmsUser is eligible for performing the given CRUD operation. Now that we’re done with this, we can move forward to see the actual implementation of the Answer API.
5. Creating Tests for AuthorizationService
We begin by creating a mock version of the AuthorizationService interface:
@Mock
private AuthorizationService authorizationService;
Now, let’s create a setup method that initializes mocks and defines a default behavior for the authorize() method in AuthorizationService, allowing it to simulate different user permissions based on roles:
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
when(this.authorizationService.authorize(any(CmsUser.class), any(ActionEnum.class)))
.thenAnswer(invocation -> {
CmsUser user = invocation.getArgument(0);
ActionEnum action = invocation.getArgument(1);
switch(user.getRole()) {
case "ADMIN": return true;
case "EDITOR": return action != ActionEnum.DELETE;
case "VIEWER": return action == ActionEnum.READ;
case "GUEST":
default: return false;
}
});
}
In this setup method, we initialize our test class’s mocks, which prepares any mocked dependencies for use before each test run. Next, we define the behavior of the authorize() method in the authorizationService mock by using when(this.authorizationService.authorize(…)).thenAnswer(…). This setup specifies a custom answer: whenever the authorize() method is called with any CmsUser and any ActionEnum, it responds according to the user’s role.
To verify the correctness, we can run the givenRoles_whenInvokingAuthorizationService_thenReturnExpectedResults() from the code repository.
6. Verifying our Implementations
Now that we are complete with everything, let’s create test methods for verifying our implementation.
@Test
public void givenRoles_whenInvokingAuthorizationService_thenReturnExpectedResults() {
CmsUser adminUser = createCmsUser("Admin User", "ADMIN");
CmsUser guestUser = createCmsUser("Guest User", "GUEST");
CmsUser editorUser = createCmsUser("Editor User", "EDITOR");
CmsUser viewerUser = createCmsUser("Viewer User", "VIEWER");
verifyAdminUserAccess(adminUser);
verifyEditorUserAccess(editorUser);
verifyViewerUserAccess(viewerUser);
verifyGuestUserAccess(guestUser);
}
Let’s focus on one of the verify methods to maintain the brevity of the article. We’ll go through the implementation to verify the admin user’s access, and we can refer to the code repository to understand the implementations for the other user roles.
We start by creating CmsUser instances for the different roles. Then, we invoke the verifyAdminUserAccess() method, passing the adminUser instance as an argument. Inside the verifyAdminUserAccess() method, we iterate through all the ActionEnum values and assert that the admin user has access to all of them. This verifies that the authorization service correctly grants the admin user full access to all actions. Implementing the other user role verification methods follows a similar pattern, and we can explore those in the code repository if we need further understanding.
7. Conclusion
In this article, we examined how Mockito’s Answer API can be used to dynamically implement role-based authorization logic in mock testing. By setting up role-based access for users, we showed how to return varied responses depending on the properties of specific parameters. This method enhances code coverage and minimizes the chances of unforeseen failures, making our tests both more dependable and effective.
As always, the full implementation of these examples can be found over on GitHub.