Wednesday, January 18, 2023

Spring Boot MockMvc Example

This article is an example of Spring Boot's MockMvc. We'll be utilising the code from Test-Driven Development with Spring Boot Starter Test. We'll move our MathFun code to the service layer and create a REST controller that will call it.

We will not dive into much detail on how to build the project as our focus will be on testing the web layer with MockMvc. The beauty of MockMvc is it doesn't start an HTTP server. It handles the incoming HTTP request and hands it off to our controller. Our code is called exactly the same way, just like a real HTTP request. With the power of annotations, Spring's injects MockMvc.

Create the Project

Create your project, head to Spring Initializr. We'll be using Maven, Java 8, Spring Boot 2.7.7, and with developer tools and web (see image) as dependencies. This is what I've got:

Project Metadata

  • Group: com.blogspot.jpllosa
  • Artifact: tdd-mockmvc
  • Name: TDD Web layer
  • Description: Test-Driven Development with MockMvc
  • Package name: com.blogspot.jpllosa

When you are ready, import the project to your IDE. I use Spring Tool Suite 4. If you are keen on the finished project, clone it here, github.com/jpllosa/tdd-mockmvc.

Spring Boot MockMvc Example
Spring Boot MockMvc Example

Create the Model

Create a package named com.blogspot.jpllosa.model and create GreatestCommonFactor. This object will be returned by our controller. Don't worry, Spring will automatically send the response body in JSON format because of the @RestController annotation.


package com.blogspot.jpllosa.model;

public class GreatestCommonFactor {
	Integer firstNumber;
	Integer secondNumber;
	Integer gcf;
	
	public Integer getFirstNumber() {
		return firstNumber;
	}
	
	public void setFirstNumber(Integer firstNumber) {
		this.firstNumber = firstNumber;
	}
	
	public Integer getSecondNumber() {
		return secondNumber;
	}
	
	public void setSecondNumber(Integer secondNumber) {
		this.secondNumber = secondNumber;
	}
	
	public Integer getGcf() {
		return gcf;
	}
	
	public void setGcf(Integer gcf) {
		this.gcf = gcf;
	}

}

Create the Service

Create a package named com.blogspot.jpllosa.service and create MathFunService. This is our service layer, the layer that handles the business logic. The service layer also serves as a traffic cop between the controller and persistence layer. Hence, this bean is annotated with @Service.


package com.blogspot.jpllosa.service;

import org.springframework.stereotype.Service;

import com.blogspot.jpllosa.model.GreatestCommonFactor;

@Service
public class MathFunService {

	public GreatestCommonFactor findGcf(int x, int y) {
		int result = getGCF(x, y);
		
		GreatestCommonFactor gcf = new GreatestCommonFactor();
		gcf.setFirstNumber(x);
		gcf.setSecondNumber(y);
		gcf.setGcf(result);
		
		return gcf;
	}
	
	public int getGCF(int x, int y) {
		return (y == 0) ? x : getGCF(y, x % y);
	}
}

Create the Controller

Last but not the least. Create a package named com.blogspot.jpllosa.controller and create MathFunController. This is our web layer. GET HTTP requests to the resource /get-gcf are routed here. The method checks for two query parameters and are automatically injected to the method parameters. Then sends it to the service layer for processing. It returns the type GreatestCommonFactor which is automatically marshalled into a JSON body as mentioned earlier.


package com.blogspot.jpllosa.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.blogspot.jpllosa.model.GreatestCommonFactor;
import com.blogspot.jpllosa.service.MathFunService;

@RestController
public class MathFunController {
	
	@Autowired
	private MathFunService service;

	@GetMapping("/get-gcf")
	public GreatestCommonFactor getGcf(@RequestParam int firstNumber, @RequestParam int secondNumber) {
		return service.findGcf(firstNumber, secondNumber);
	}
}

Running the App

Now that everything is wired up, we should be able to run the application and see the response. To run the app in STS4, right click, Run As + Spring Boot App.


$ curl -G -d "firstNumber=12" -d "secondNumber=16" http://localhost:8080/get-gcf
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    44    0    44    0     0   1419      0 --:--:-- --:--:-- --:--:--  1419{"firstNumber":12,"secondNumber":16,"gcf":4}

Above, we are finding the GCF of 12 and 16. The GCF is 4.


$ curl "http://localhost:8080/get-gcf?firstNumber=0&secondNumber=6"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    42    0    42    0     0   2800      0 --:--:-- --:--:-- --:--:--  2800{"firstNumber":0,"secondNumber":6,"gcf":6}

Above is another way of sending the curl command, we are finding the GCF of 0 and 6. The GCF is 6. If you don't have curl, you can use the browser or Postman to send your HTTP request.

Spring Boot MockMvc

Now the thing we have been waiting for. Create a package named com.blogspot.jpllosa.controller under src/test/java/ and create MathFunControllerTest. This is the class under test. To run the test in STS4, right click, Run As + JUnit test.


package com.blogspot.jpllosa.controller;

import static org.hamcrest.CoreMatchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc
public class MathFunControllerTest {
	
	@Autowired
	private MockMvc mockMvc;
	
	@Test
	public void getGcfTest() throws Exception {
		this.mockMvc.perform(get("/get-gcf?firstNumber=12&secondNumber=16"))
			.andDo(print())
			.andExpect(status().isOk())
			.andExpect(jsonPath("$.firstNumber", is(12)))
			.andExpect(jsonPath("$.secondNumber", is(16)))
			.andExpect(jsonPath("$.gcf", is(4)));
	}

}

Now that we have seen the code, let's explain what it does. The @SpringBootTest annotation tells Spring the following:

  • To use the default ContextLoader, in this case, it's SpringBootContextLoader.
  • To automatically search for @SpringBootConfiguration.
  • To register a TestRestTemplate and/or WebTestClient. But we won't be using these.
  • To load a WebApplicationContext and provide a mock servlet environment. Used in conjunction with @AutoConfigureMockMvc for MockMvc based testing. As the name suggests, Spring will auto-confiure MockMvc and automatically inject (@Autowired) it for us.

The @Test annotation tells JUnit that the method is to be run as a test case. What does this method do? Using MockMvc, it performs a HTTP GET request on /get-gcf resource with two query parameters. It then prints the response and checks for a 200 status. By using jsonPath dot notation, we can assert the response body values with the expected values. The dollar sign represents the root member object.

Spring Boot MockMvc Example
JUnit Passed

print() output


2023-01-18 07:52:27.115  INFO 19520 --- [           main] c.b.j.controller.MathFunControllerTest   : Started MathFunControllerTest in 2.381 seconds (JVM running for 4.218)

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /get-gcf
       Parameters = {firstNumber=[12], secondNumber=[16]}
          Headers = []
             Body = null
    Session Attrs = {}

Handler:
             Type = com.blogspot.jpllosa.controller.MathFunController
           Method = com.blogspot.jpllosa.controller.MathFunController#getGcf(int, int)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"firstNumber":12,"secondNumber":16,"gcf":4}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

There you have it. A simple example of how to use MockMvc.