Spring Boot: REST controller Test example

How to test the @RestController with Spring Boot

Spring Boot: REST controller Test example

updated 02.2022

In my Spring Boot - Angular showcase you can find some examples of REST controller tests.

The @RestController used for the example is the following:

``` java @RestController // we allow cors requests from our frontend environment // note the curly braces that create an array of strings ... required by the annotation @CrossOrigin(origins =  {"${app.dev.frontend.local"}) public class HelloController {

// simple GET response for our example purpose, we return a JSON structure @RequestMapping(value = "/message", produces = MediaType.APPLICATIONJSONVALUE) public Map<String, String> index() { return Collections.singletonMap("message", "Greetings from Spring Boot!"); } }


## Test the controller using an embedded server (integration tests)

With this approach, Spring starts an embedded server to test your REST service.

To create these tests you have to add a dependency to :

``` xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
</dependency>

In your test, you have to define a webEnvironment, in our case we create an environment with a random port number.

Defining the webEnvironment we can wire the TestRestTemplate that allows us to execute REST requests. TestRestTemplate is fault-tolerant and can be used with Basic authentication headers. It doesn't extend RestTemplateif you encounter issues during r tests you should maybe try RestTemplate.

/**
 * The goal of this class is to show how the Embedded Server is used to test the REST service
 */

// SpringBootTest launch an instance of our application for tests purposes
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class HelloControllerEmbeddedServerTest {
  @Autowired
  private HelloController helloController;

  // inject the runtime port, it requires the webEnvironment
  @LocalServerPort
  private int port;

  // we use TestRestTemplate, it's an alternative to RestTemplate specific for tests
  // to use this template a webEnvironment is mandatory
  @Autowired
  private TestRestTemplate restTemplate;

  @Test
  void index() {
    // we test that our controller is not null
    Assertions.assertThat(helloController).isNotNull();
  }

  @Test
  void indexResultTest() {
    Assertions.assertThat(restTemplate
      .getForObject("http://localhost:" + port + "/message", String.class)).contains("from Spring Boot");
  }
}

When you run the test you can notice in your console that Spring Boot runs a Tomcat Server.

``` bash INFO 4230 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 0 (http) INFO 4230 --- [main] o.apache.catalina.core.StandardService   : Starting service [Tomcat] INFO 4230 --- [main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.43] INFO 4230 --- [main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext INFO 4230 --- [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1761 ms


If you are trying to test GET methods with payload, this method could give you headaches.

`RestTemplateTest` doesn't like to add payloads to the GET request and you will receive an error response, you can read more in my post about the [Spring Boot GET Test limitation](https://marmo.dev/spring-boot-test-get-body).

## MockMvc, testing without an embedded server

The previous controller could be tested with `@MockMvc`, this allows us to have tested the `RestController` without the overhead of a server (pushing us in the integration tests domain).

``` java
/**
 * The goal of this class is to test the controller using a MockMvc object without an embedded server
 */
@SpringBootTest
@AutoConfigureMockMvc // we mock the http request and we don't need a server
public class HelloControllerMockMvcTest {

    @Autowired
    private MockMvc mockMvc; // injected with @AutoConfigureMockMvc

    @Test
    public void shouldReturnOurText() throws Exception {
        this.mockMvc
                .perform(get("/message")) // perform a request that can be chained
                .andDo(print()) // we log the result
                .andExpect(content().string(containsString(" from Spring"))); // we check that the Body of the answer contains our expectation
    }
}

In this case Spring initializes a test Servlet without embedding a full server, from the console:

``` bash INFO 4589 --- [main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor' INFO 4589 --- [main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page: class path resource [static/index.html] INFO 4589 --- [main] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet '' INFO 4589 --- [main] o.s.t.web.servlet.TestDispatcherServlet  : Initializing Servlet '' INFO 4589 --- [main] o.s.t.web.servlet.TestDispatcherServlet  : Completed initialization in 0 ms


With the `.andDo(print())` instruction many details are printed in the console (extract):
``` bash
...
ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json"]
     Content type = application/json
             Body = {"message":"Greetings from Spring Boot!"}
...

(Old) Deep dive: @WebMvcTest, how it works

The annotation @WebMvcTest configure only the components that usually interest the web development.

As shown in the image @Service and @Repositoryare not configured.

When we call the @Service from the @Controller we return the mocked object.

Controller example

This is a very simple controller that calls a service and returns a custom object containing a text value:

``` java

@RestController public class SimpleController {

private SimpleService simpleService;

public SimpleController(SimpleService simpleService) { this.simpleService = simpleService; }

@GetMapping(value = "/simple",produces = MediaType.APPLICATIONJSONVALUE) public ResponseEntity<StringJsonObject> simpleResult() { return ResponseEntity.ok(simpleService.getText()); } }


Here the service code:

``` java

@Service
public class SimpleServiceImpl implements SimpleService{
    @Override
    public StringJsonObject getText(){
        return new StringJsonObject("Cool!");
    }
}

The returned object:

``` java public class StringJsonObject {

private String content;

public StringJsonObject(String content) { this.content = content; }

public String getContent() { return content; } }


## The test with comments

Here the code used to test the controller:

``` java

// SpringRunner is an alias of SpringJUnit4ClassRunner
// it's a Spring extension of JUnit that handles the TestContext
@RunWith(SpringRunner.class)

// we test only the SimpleController
@WebMvcTest(SimpleController.class)

public class SimpleControllerTest {

    // we inject the server side Spring MVC test support
    @Autowired
    private MockMvc mockMvc;

    // we mock the service, here we test only the controller
    // @MockBean is a Spring annotation that depends on mockito framework
    @MockBean
    private SimpleService simpleServiceMocked;

    @Test
    public void simpleResult() throws Exception {

        // this is the expected JSON answer
        String responseBody = "{\"content\":\"Hello World from Spring!\"}";

        // we set the result of the mocked service
        given(simpleServiceMocked.getText())
                .willReturn(new StringJsonObject("Hello World from Spring!"));

        // the test is executed:
        // perform: it executes the request and returns a ResultActions object
        // accept: type of media accepted as response
        // andExpect: ResultMatcher object that defines some expectations
        this.mockMvc.perform(get("/simple")
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().string(responseBody));
    }
}