In the previous article, we integrated the Razorpay payment gateway with our Spring Boot Angular application. Now, we are gonna unit test one of the REST controller using Mockito.
Introduction
JUnit is an open-source unit testing framework for Java that is used to write and run repeatable automated tests. Unit testing is one of the best test methods for regression testing.
Mockito is an open-source testing framework for Java that allows the creation of test double objects in automated unit tests for the purpose of test-driven development or behavior-driven development.
Implementation
Add Dependencies
Let’s add the spring-security-test
dependency to our pom.xml
since it is not part of the spring-boot-starter-test
dependency. We need this dependency to create a MockCustomUserSecurityContextFactory
for the Junit tests since some of the API endpoints that we are going to unit test have method-level security.
pom.xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
Create Utility Class
This utility class is responsible for creating the User Entity mocks that will be used for creating request objects and mock security contexts.
MockUserUtils.java
package com.javachinna.config;
import java.util.Set;
import com.javachinna.model.Role;
import com.javachinna.model.User;
public class MockUserUtils {
private MockUserUtils() {
}
/**
*
*/
public static User getMockUser(String username) {
User user = new User();
user.setId(1L);
user.setEmail(username);
user.setPassword("secret");
Role role = new Role();
role.setName(Role.ROLE_PRE_VERIFICATION_USER);
user.setRoles(Set.of(role));
user.setEnabled(true);
user.setSecret("secret");
return user;
}
}
Test with Mock User
Using @WithMockUser
Annotation
The code verification REST API is having method-based security. So, only authenticated users with PRE_VERIFICATION_USER
role can access this endpoint.
If we are not using a custom Authentication
principal, then we can use @WithMockUser
annotation to run the test as a specific user with PRE_VERIFICATION_USER
role as shown below.
@Test
@WithMockUser(username="chinna",roles={"PRE_VERIFICATION_USER"})
public void testVerifyCodeWhenCodeIsValid() {
...
}
Or with just roles
@Test
@WithMockUser(roles={"PRE_VERIFICATION_USER"})
public void testVerifyCodeWhenCodeIsValid() {
...
}
Or we can also place the annotation at the class level and every test will use the specified user. So that, we don’t need to annotate each test with @WithMockUser
annotation
@SpringBootTest
@AutoConfigureMockMvc
@WithMockUser(username="admin",roles={"USER","ADMIN","PRE_VERIFICATION_USER"})
class AuthControllerTest {
...
}
If we are using a custom Authentication
principal, then there are 2 options. Either we can use @WithUserDetails
annotation or we can create our own custom annotation
Using @WithUserDetails
Annotation
The custom principal is often times returned by a custom UserDetailsService
that returns an object that implements both UserDetails
and the custom type. In our case, it is LocalUser
which extends org.springframework.security.core.userdetails.User
and implements OAuth2User
& OidcUser
for social authentication support.
For situations like this, it is useful to create the test user using the custom UserDetailsService
. That is exactly what @WithUserDetails
does.
@WithUserDetails
would allow us to use a custom UserDetailsService
to create our Authentication
principal but it requires the user to exist in the database.
@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="localUserDetailService")
public void testVerifyCodeWhenCodeIsValid() {
...
}
This test would look up the username of customUsername
using the UserDetailsService
with the bean name localUserDetailService
. Both value
and userDetailsServiceBeanName
fields are optional. If we don’t specify, then this test would look up the username of user
using the UserDetailsService
Using Custom Annotation
We can create our own annotation that uses the @WithSecurityContext
to create any SecurityContext
we want. For example, we might create an annotation named @WithMockCustomUser
as shown below:
WithMockCustomUser.java
package com.javachinna.config;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.springframework.security.test.context.support.WithSecurityContext;
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
String username() default "JavaChinna";
}
You can see that @WithMockCustomUser
is annotated with the @WithSecurityContext
annotation. This is what signals to Spring Security Test support that we intend to create a SecurityContext
for the test. The @WithSecurityContext
annotation requires we specify a SecurityContextFactory
that will create a new SecurityContext
given our @WithMockCustomUser
annotation. You can find our WithMockCustomUserSecurityContextFactory
implementation below:
WithMockCustomUserSecurityContextFactory.java
package com.javachinna.config;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.test.context.support.WithSecurityContextFactory;
import com.javachinna.dto.LocalUser;
import com.javachinna.model.User;
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> {
@Override
public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
User user = MockUserUtils.getMockUser(customUser.username());
LocalUser localUser = LocalUser.create(user, null, null, null);
Authentication auth = new UsernamePasswordAuthenticationToken(localUser, user.getPassword(), localUser.getAuthorities());
context.setAuthentication(auth);
return context;
}
}
We can now annotate a test class or a test method with our new annotation and Spring Security’s WithSecurityContextTestExecutionListener
will ensure that our SecurityContext
is populated appropriately.
Unit Testing Authentication Controller
Since application security is one of the critical aspects of an application, it’s our responsibility to unit test to make sure that it is defect-free and working as expected. Hence, we are gonna write some unit test cases for our AuthConroller
in this Spring Boot + Angular + MySQL Maven Application.
The AuthConroller
exposes 3 POST APIs for User Login, Registration, and TOTP verification requests. Let’s create AuthConrollerTest
class to unit test these 3 endpoints.
AuthControllerTest.java
@SpringBootTest
annotation can be specified on a test class that runs Spring Boot based tests. It provides the following features over and above the regular Spring TestContext Framework:
- Uses
SpringBootContextLoader
as the defaultContextLoader
when no specific@ContextConfiguration(loader=...)
is defined. - Automatically searches for a
@SpringBootConfiguration
when nested@Configuration
is not used, and no explicitclasses
are specified. - Allows custom
Environment
properties to be defined using theproperties attribute
. - Allows application arguments to be defined using the
args attribute
. - Provides support for different
webEnvironment
modes, including the ability to start a fully running web server listening on adefined
orrandom
port. - Registers a
TestRestTemplate
and/orWebTestClient
bean for use in web tests that are using a fully running web server.
@AutoConfigureMockMvc
annotation can be applied to a test class to enable and configure auto-configuration of MockMvc
which provides the server-side Spring MVC test support.
Note: You can also use @WebMvcTest
annotation that focuses on Spring MVC components. This annotation will disable full auto-configuration and only apply configuration relevant to MVC tests. If you are looking to load your full application configuration and use MockMVC
, you should consider @SpringBootTest
combining with @AutoConfigureMockMvc
rather than this annotation.
@MockBean
annotation can be used to add mocks to a Spring ApplicationContext
. It can be used as a class-level annotation or on fields in either @Configuration
classes or test classes that are @RunWith
the SpringRunner
. Mocks can be registered by type or by bean name
. Any existing single bean of the same type defined in the context will be replaced by the mock. If no existing bean is defined a new one will be added.
package com.javachinna.controller;
import static org.mockito.ArgumentMatchers.any;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.test.web.servlet.MockMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.javachinna.config.MockUserUtils;
import com.javachinna.config.WithMockCustomUser;
import com.javachinna.dto.LocalUser;
import com.javachinna.dto.LoginRequest;
import com.javachinna.dto.SignUpRequest;
import com.javachinna.dto.SocialProvider;
import com.javachinna.exception.UserAlreadyExistAuthenticationException;
import com.javachinna.model.User;
import com.javachinna.service.UserService;
import dev.samstevens.totp.code.CodeVerifier;;
@SpringBootTest
@AutoConfigureMockMvc
class AuthControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@MockBean
private CodeVerifier verifier;
@MockBean
private AuthenticationManager authenticationManager;
private static User user = MockUserUtils.getMockUser("JavaChinna");
private static ObjectMapper mapper = new ObjectMapper();
@Test
public void testAuthenticateUser() throws Exception {
LocalUser localUser = LocalUser.create(user, null, null, null);
LoginRequest loginRequest = new LoginRequest(user.getEmail(), user.getPassword());
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(localUser, null);
Mockito.when(authenticationManager.authenticate(Mockito.any(UsernamePasswordAuthenticationToken.class))).thenReturn(authentication);
String json = mapper.writeValueAsString(loginRequest);
mockMvc.perform(post("/api/auth/signin").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.authenticated").value("true")).andExpect(jsonPath("$.accessToken").isNotEmpty());
// Test when user 2fa is enabled
user.setUsing2FA(true);
mockMvc.perform(post("/api/auth/signin").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.authenticated").value("false")).andExpect(jsonPath("$.user").doesNotExist());
}
@Test
public void testRegisterUser() throws Exception {
SignUpRequest signUpRequest = new SignUpRequest("1234", "JavaChinna", user.getEmail(), user.getPassword(), user.getPassword(), SocialProvider.FACEBOOK);
Mockito.when(userService.registerNewUser(any(SignUpRequest.class))).thenReturn(user);
String json = mapper.writeValueAsString(signUpRequest);
mockMvc.perform(post("/api/auth/signup").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.success").value("true")).andExpect(jsonPath("$.message").value("User registered successfully"));
// Test when user provided email already exists in the database
Mockito.when(userService.registerNewUser(any(SignUpRequest.class))).thenThrow(new UserAlreadyExistAuthenticationException("exists"));
json = mapper.writeValueAsString(signUpRequest);
mockMvc.perform(post("/api/auth/signup").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest()).andExpect(jsonPath("$.success").value("false")).andExpect(jsonPath("$.message").value("Email Address already in use!"));
}
@Test
@WithMockCustomUser
public void testVerifyCodeWhenCodeIsNotValid() throws Exception {
Mockito.when(verifier.isValidCode(Mockito.anyString(), Mockito.anyString())).thenReturn(false);
String json = mapper.writeValueAsString("443322");
mockMvc.perform(post("/api/auth/verify").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest()).andExpect(jsonPath("$.success").value("false")).andExpect(jsonPath("$.message").value("Invalid Code!"));
}
@Test
@WithMockCustomUser
public void testVerifyCodeWhenCodeIsValid() throws Exception {
Mockito.when(verifier.isValidCode(Mockito.anyString(), Mockito.anyString())).thenReturn(true);
String json = mapper.writeValueAsString("443322");
mockMvc.perform(post("/api/auth/verify").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.authenticated").value("true")).andExpect(jsonPath("$.accessToken").isNotEmpty())
.andExpect(jsonPath("$.user").exists());
}
}
Note: If you are looking for a simple solution and don’t want to use any of the Mock User/Custom annotations to test the REST controller, then you can have a look at Unit Test REST Controller with Spring Security using Mock Authentication which is just a modified version of the same AuthControllerTest
class.
Now, let’s dive deep into each method and see what it does for a better understanding in the following sections.
Login API Unit Test Cases
This test method is responsible for unit testing the SignIn
API. It covers the following 2 scenarios.
- Test when 2FA is not enabled.
- Expected result: HTTP Status 200 Ok response with
access token
andauthenticated=true
in the response body.
- Expected result: HTTP Status 200 Ok response with
- Test when 2FA is enabled.
- Expected result: HTTP Status 200 Ok response with
authenticated=false
in the response body.
- Expected result: HTTP Status 200 Ok response with
To keep it simple, I have covered both use cases in the same test method. However, it is always a good practice to have one test method for one use case as per the single-responsibility principle (SRP).
@Test
public void testAuthenticateUser() throws Exception {
LocalUser localUser = LocalUser.create(user, null, null, null);
LoginRequest loginRequest = new LoginRequest(user.getEmail(), user.getPassword());
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(localUser, null);
Mockito.when(authenticationManager.authenticate(Mockito.any(UsernamePasswordAuthenticationToken.class))).thenReturn(authentication);
String json = mapper.writeValueAsString(loginRequest);
mockMvc.perform(post("/api/auth/signin").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.authenticated").value("true")).andExpect(jsonPath("$.accessToken").isNotEmpty());
// Test when user 2fa is enabled
user.setUsing2FA(true);
mockMvc.perform(post("/api/auth/signin").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.authenticated").value("false")).andExpect(jsonPath("$.user").doesNotExist());
}
Points to be noted:
Mockito.when()
method enables stubbing methods. Use it when you want the mock to return a particular value when a particular method is called. Simply put: “When the x method is called then return y”.- Login API makes use of
AuthenticationManager.authenticate()
method to perform the authentication. Since we don’t want to call this actual method during our test, we have mocked theAuthenticationManager
using@MockBean
annotation and stubbed theauthenticate()
method call withMockito.when()
method. So that, whenever this method is called, Mockito will return our mockauthentication
object.
- Login API makes use of
MockMvc.perform()
method used to perform a request and return a type that allows chaining further actions, such as asserting expectations, on the result.MockMvcRequestBuilders.post()
method is used to build an HTTP post requestResultActions.andExpect()
method is used to perform an expectation on the returned responseMockMvcResultMatchers.jsonPath()
method that allows access to response body assertions using a JsonPath expression to inspect a specific subset of the body. The JSON path expression can be a parameterized string using formatting specifiers as defined inString.format(String, Object)
.
User Registration API Unit Test Cases
This test method is responsible for unit testing the SignUP
API. It covers the following 2 scenarios.
- Test when user-provided email does not exist in the database
- Expected result: HTTP Status 200 Ok response with
success
message in the response body.
- Expected result: HTTP Status 200 Ok response with
- Test when user-provided email already exists in the database
- Expected result: HTTP Status 400 BadRequest response with
error
message in the response body.
- Expected result: HTTP Status 400 BadRequest response with
@Test
public void testRegisterUser() throws Exception {
SignUpRequest signUpRequest = new SignUpRequest("1234", "JavaChinna", user.getEmail(), user.getPassword(), user.getPassword(), SocialProvider.FACEBOOK);
Mockito.when(userService.registerNewUser(any(SignUpRequest.class))).thenReturn(user);
String json = mapper.writeValueAsString(signUpRequest);
mockMvc.perform(post("/api/auth/signup").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.success").value("true")).andExpect(jsonPath("$.message").value("User registered successfully"));
// Test when user provided email already exists in the database
Mockito.when(userService.registerNewUser(any(SignUpRequest.class))).thenThrow(new UserAlreadyExistAuthenticationException("exists"));
json = mapper.writeValueAsString(signUpRequest);
mockMvc.perform(post("/api/auth/signup").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest()).andExpect(jsonPath("$.success").value("false")).andExpect(jsonPath("$.message").value("Email Address already in use!"));
}
Code Verification API Unit Test Cases
This test method is responsible for unit testing the Verify
API. It tests if the application returns a HTTP Status 400 BadRequest
with Invalid Code error message
in the response when the code is not valid.
@Test
@WithMockCustomUser
public void testVerifyCodeWhenCodeIsNotValid() throws Exception {
Mockito.when(verifier.isValidCode(Mockito.anyString(), Mockito.anyString())).thenReturn(false);
String json = mapper.writeValueAsString("443322");
mockMvc.perform(post("/api/auth/verify").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest()).andExpect(jsonPath("$.success").value("false")).andExpect(jsonPath("$.message").value("Invalid Code!"));
}
This test checks if the application returns a HTTP Status 200 Ok
with authenticated=true
in the response when the code is valid.
@Test
@WithMockCustomUser
public void testVerifyCodeWhenCodeIsValid() throws Exception {
Mockito.when(verifier.isValidCode(Mockito.anyString(), Mockito.anyString())).thenReturn(true);
String json = mapper.writeValueAsString("443322");
mockMvc.perform(post("/api/auth/verify").contentType(MediaType.APPLICATION_JSON).characterEncoding("utf-8").content(json).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("$.authenticated").value("true")).andExpect(jsonPath("$.accessToken").isNotEmpty())
.andExpect(jsonPath("$.user").exists());
}
Run JUnit Tests
References
https://docs.spring.io/spring-security/site/docs/current/reference/html5/#test-method-withmockuser
Source Code
https://github.com/JavaChinna/angular-spring-boot-razorpay-integration
Conclusion
That’s all folks. In this article, we have implemented unit test cases for our REST controller using Junit 5 and Mockito.
Thank you for reading.