How are you testing your Controllers? Are you ignore unit tests for them? Or you just write some time consuming, heavyweight integration tests. Let’s take a look to MockMvcTest.

Introduction to @MockMvcTest

Spring offers few different options to test our web layer. These can be combined under two testing strategy. Integration testing and unit testing. Both have their own advantages and profits. Integration tests are useful to test your application’s business logic as much as similar to production environment. But they need all application context up, they are heavyweight(time consuming) and not so practical. Unit testing easy to create, practical and lightweight. No need to application context but hard to maintain because you need mock and stubs classes. Also real profit of unit testing is not just test your units. It is a good indicator of how’s your design. If it is hard to write a test for a class/method, then it means you have wrong dependencies, complex methods or objects.

Today I will give an example to web layer testing which is a bit middle point for integration and unit testing. It is not required all application context. However it needs web layer beans and their dependencies in application context. So it needs some mocks which similar to unit tests but also test web layer serialization(Jackson mappers), http status codes(validations) so a bit similar to integration tests in that perspective.

Configuration

Before start to write your tests for your controllers, need some configurations for @MockMvcTest. Let’s have a look the below basic gradle.build file;

/*
 * This file was generated by the Gradle 'init' task.
 *
 * This generated file contains a sample Java project to get you started.
 * For more details take a look at the Java Quickstart chapter in the Gradle
 * user guide available at https://docs.gradle.org/4.6/userguide/tutorial_java_projects.html
 */
plugins {
    id "org.springframework.boot" version "2.1.3.RELEASE"
}

apply plugin: 'idea'
apply plugin: 'java'
apply plugin: 'io.spring.dependency-management'

ext {
    springBootVersion = '2.1.3.RELEASE'
}
// In this section you declare where to find the dependencies of your project
repositories {
    mavenCentral()
}

bootJar {
    mainClassName = 'com.softwarelabs.App'
    baseName = 'spring-boot-integration-test'
    version = '0.1.0'
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compileOnly('org.projectlombok:lombok')

    testCompile("org.springframework.boot:spring-boot-starter-test")
}

task wrapper(type: Wrapper) {
    gradleVersion = '4.6'
}

For @MockMvcTest we just need to add testCompile("org.springframework.boot:spring-boot-starter-test") to our gradle file. Other configurations are to create a basic web project on spring boot.

How to write a mockMvc test

Now our project is configured with necessary libraries. Need a controller and a piece of business logic to test. So created a controller, service and domain objects to test the end points with @MockMvcTest. So let’s check below code.

@RunWith(SpringRunner.class)
@WebMvcTest(ProductController.class)
public class ProductControllerWebMvcTest {

	private static final Long productId = 1L;

	@Autowired private MockMvc mockMvc;
	@Autowired private ObjectMapper objectMapper;
	@MockBean private ProductService productService;
	@MockBean private ProductMapper productMapper;

	@Test
	public void createProductReturnHttpStatusCode200_ifProductIsValid() throws Exception {
		//To be provided
	}

	@Test
	public void returnProductWithHttpStatusCode200_ifProductIsExist() throws Exception {
		//To be provided
	}

}

As you see @WebMvcTest(ProductController.class) we don’t even need all web layer application context. We just need our controller and its dependencies. There are few important points to determine which classes should be mocked. We need to mock productService and productMapper. Because these beans are using as a subfields or in other words depended by ProductController class.

Another important point to keep in my that, the beans which are defined as @MockBean should be interface, otherwise ProductController bean can’t be initialized. If your dependencies are not interface or not implementing an interface then you most likely will get java.lang.IllegalStateException: Failed to load ApplicationContext exception in detailed you will see error message as

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'productController'.

We are also using 2 autowired beans mockMvc and objectMapper so mockMvc helps us to call our endpoint and objectMapper helps us to convert java object to json which will be converted by RequestBody in our controller.

Test for @PostMapping

In below test we are creating a productRequest object and send the request to endpoint /v1/product with POST http method. Also we have mocks as

when(productService.createProduct(any(), any())).thenReturn(product);
when(productMapper.mapToProductDto(product)).thenReturn(productDto);

So we setUp our scenario as a success response.


@Test
	public void createProductReturnHttpStatusCode200_ifProductIsValid() throws Exception {
		String productName = "Product-1";
		IProductPort.ProductRequest productRequest =
				new IProductPort.ProductRequest().setId(productId).setName(productName);
		String json = objectMapper.writeValueAsString(productRequest);

		Product product = new Product(productId, productName);
		ProductDto productDto = new ProductDto(productName);

		when(productService.createProduct(any(), any())).thenReturn(product);
		when(productMapper.mapToProductDto(product)).thenReturn(productDto);

		this.mockMvc
				.perform(
						post("/v1/product")
								.contentType(MediaType.APPLICATION_JSON)
								.content(json)
								.accept(MediaType.APPLICATION_JSON))
				.andDo(print())
				.andExpect(MockMvcResultMatchers.status().isOk())
				.andExpect(content().string(containsString("Success")))
				.andExpect(content().string(containsString(productId.toString())))
				.andExpect(content().string(containsString(productName)));
	}

Because we sent a valid product, we are expecting status code as HTTP 200 and checking other fields at response which should be in success response.

Test for @GetMapping

For GET request nothings change a lot. We are just sending the productId as a request parameter(as a pathVariable) and expect success response if our mock return as success for that productId.


@Test
	public void returnProductWithHttpStatusCode200_ifProductIsExist() throws Exception {
		String productName = "Product-" + productId;
		Product product = new Product(productId, productName);
		ProductDto productDto = new ProductDto(productName);

		when(productService.getProduct(any())).thenReturn(product);
		when(productMapper.mapToProductDto(product)).thenReturn(productDto);

		this.mockMvc
				.perform(
						get("/v1/product/" + productId)
								.contentType(MediaType.APPLICATION_JSON)
								.accept(MediaType.APPLICATION_JSON))
				.andDo(print())
				.andExpect(MockMvcResultMatchers.status().isOk())
				.andExpect(content().string(containsString("Success")))
				.andExpect(content().string(containsString(productId.toString())))
				.andExpect(content().string(containsString(productName)));
	}

Notes

@MockMvcTest is a great way to test your Controller without initialize all application context. You will just initialize the bean you need and mock other dependencies. It is a good way to test your mappings, validations or even application accepted media type problems.

Result

Spring Boot is only instantiating the web layer, not the whole context. In an application with multiple controllers you can even ask for just one to be instantiated, for example @WebMvcTest(ProductController.class).

So Spring is not start the server at all, just test the web layer. In our example just one controller and its dependencies.

You can find the all project on Github

References

https://spring.io/guides/gs/testing-web/

Happy coding :)


muzir

A software developer who loves open source and try to contribute few of them.