How to Write Junit 5 Test Cases for Spring REST Controller using Mockito

In the previous article, we have 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 gonna unit test are having 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 API’s 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 default ContextLoader when no specific @ContextConfiguration(loader=...) is defined.
  • Automatically searches for a @SpringBootConfiguration when nested @Configuration is not used, and no explicit classes are specified.
  • Allows custom Environment properties to be defined using the properties 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 a defined or random port.
  • Registers a TestRestTemplate and/or WebTestClient 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. Using this annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests. If you are looking to load your full application configuration and use MockMVC, you should consider @SpringBootTest combined 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 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 and authenticated=true in the response body.
  • Test when 2FA is enbaled.
    • Expected result: Http Status 200 Ok response with authenticated=false in the response body.

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 particular value when 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 the AuthenticationManager using @MockBean annotation and stubbed the authenticate() method call with Mockito.when() method. So that, whenever this method is called, Mockito will return our mock authentication object.
  • 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 a HTTP post request
  • ResultActions.andExpect() method is used to perform an expectation on the returned response
  • MockMvcResultMatchers.jsonPath() method 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 in String.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.
  • Test when user provided email already exists in the database
    • Expected result: Http Status 400 BadRequest response with error message in the response body.
@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 Verfication 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

Spring REST API Junit Test Results

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.

Read Next: Test REST Controller with Spring Security using Mock Authentication or Disable Security in JUnit Tests

Leave a Reply