Guide to Testing Controllers in Spring Boot

Guide to Testing Controllers in Spring Boot

There are different ways to test your Controller (Web or API Layer) classes in Spring Boot, some provide support to write pure Unit Tests and some others are more useful for Integration Tests. Within this post, I’ll cover the main three test approaches available for Spring: using MockMVC in standalone mode, MockMVC together with SpringRunner, and using SpringBootTest.

Updated: Code examples use Java 10 and Spring Boot 2. More info.

Introduction

There are a few different approaches to testing available in Spring Boot. It’s a framework that’s constantly evolving, so more options arise in new versions at the same time that old ones are kept for the sake of backward compatibility. The result: multiple ways of testing the same part of our code, and some unclarity about when to use what. Within this post, I’ll help you understand the different alternatives, the reasons why they are available and when it’s better to use each one.

This article focuses on Controller testing since it’s the most unclear part, where mocking objects is possible at different levels.

The sample application

We’ll use some sample code through this post to put into practice the different concepts covered.

All the code in this post is available on GitHub: Spring Boot Testing Strategies. If you find it useful, please give it a star!

In summary, it’s just a repository of entities -superheroes- exposed through a REST API. It’s important to list also some particularities of the application to further understand what happens when using the different strategies:

  • If a superhero can't be found by their identifier, a NonExistingHeroException is thrown. There is a Spring's @RestControllerAdvice that will intercept that exception and transform it into a 404 status code - NOT_FOUND.
  • There is a SuperHeroFilter class that will be used in our HTTP communication to add a header to the HTTP Response: X-SUPERHERO-APP.
Get the book Practical Software Architecture

Server and Client-Side Tests

To start with, we can separate server-side and client-side tests.

Server-side tests are the most extended way of testing: you perform your request and you want to check how the server behaves, the response composition, the response content, etc.

The client-side tests are not so common, they are useful when you want to verify the request composition and actions. In these tests, you mock the server behavior and then you call some code (on your side) that indirectly will perform a request to that server. That’s exactly what you want to test, you want to verify that there was a request and the contents of that request. You don’t care about the response contents (you mocked that part). Unfortunately, there are not many good examples of this. Even if you check the official examples, they are not so helpful (see the Javadoc comments). Anyway, the important idea here is that they can be used when you’re writing a client application and you want to verify the requests from your side to the outside world.

We’ll focus on server-side Tests, which are the ones to verify how the server logic works. In this case, you normally mock the requests, and you want to check how your server logic reacts. These kind of tests are tightly related to the Controller layer in your application since it’s the part of Spring that takes care of handling the HTTP requests.

Server-Side Tests

If we zoom inside server-side tests, there are two main strategies we can identify in Spring: writing Controller tests using the MockMVC approach, or making use of RestTemplate. You should favor the first strategy (MockMVC) if you want to code a real Unit Test, whereas you should make use of RestTemplate if you intend to write an Integration Test. The reason is that with MockMVC we can fine-grain our assertions for the Controller. RestTemplate, on the other hand, will use the Spring’s WebApplicationContext (partly or fully, depends on using the Standalone mode or not). Let’s explain these two strategies in more detail.

Inside-Server Tests

We can test directly our Controller logic without needing a web server to be running. That’s what I call inside-server testing, and it’s closer to the definition of a Unit Test. To make it possible, you need to mock the entire web server behavior, so somehow you’re missing parts to be tested in our application. But don’t worry, because those parts can be perfectly covered with an Integration Test.

Strategy 1: MockMVC in Standalone Mode

Test MockMVC Standalone

In Spring, you can write an inside-server test if you use MockMVC in standalone mode, so you’re not loading any context. Let’s see an example of this.

MockMVC standalone code example

MockMVC Standalone approach - SuperHeroController
@RunWith(MockitoJUnitRunner.class)
public class SuperHeroControllerMockMvcStandaloneTest {

    private MockMvc mvc;

    @Mock
    private SuperHeroRepository superHeroRepository;

    @InjectMocks
    private SuperHeroController superHeroController;

    // This object will be magically initialized by the initFields method below.
    private JacksonTester<SuperHero> jsonSuperHero;

    @Before
    public void setup() {
        // We would need this line if we would not use MockitoJUnitRunner
        // MockitoAnnotations.initMocks(this);
        // Initializes the JacksonTester
        JacksonTester.initFields(this, new ObjectMapper());
        // MockMvc standalone approach
        mvc = MockMvcBuilders.standaloneSetup(superHeroController)
                .setControllerAdvice(new SuperHeroExceptionHandler())
                .addFilters(new SuperHeroFilter())
                .build();
    }

    @Test
    public void canRetrieveByIdWhenExists() throws Exception {
        // given
        given(superHeroRepository.getSuperHero(2))
                .willReturn(new SuperHero("Rob", "Mannon", "RobotMan"));

        // when
        MockHttpServletResponse response = mvc.perform(
                get("/superheroes/2")
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();

        // then
        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getContentAsString()).isEqualTo(
                jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
        );
    }

    @Test
    public void canRetrieveByIdWhenDoesNotExist() throws Exception {
        // given
        given(superHeroRepository.getSuperHero(2))
                .willThrow(new NonExistingHeroException());

        // when
        MockHttpServletResponse response = mvc.perform(
                get("/superheroes/2")
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();

        // then
        assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
        assertThat(response.getContentAsString()).isEmpty();
    }

    @Test
    public void canRetrieveByNameWhenExists() throws Exception {
        // given
        given(superHeroRepository.getSuperHero("RobotMan"))
                .willReturn(Optional.of(new SuperHero("Rob", "Mannon", "RobotMan")));

        // when
        MockHttpServletResponse response = mvc.perform(
                get("/superheroes/?name=RobotMan")
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();

        // then
        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getContentAsString()).isEqualTo(
                jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
        );
    }

    @Test
    public void canRetrieveByNameWhenDoesNotExist() throws Exception {
        // given
        given(superHeroRepository.getSuperHero("RobotMan"))
                .willReturn(Optional.empty());

        // when
        MockHttpServletResponse response = mvc.perform(
                get("/superheroes/?name=RobotMan")
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();

        // then
        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getContentAsString()).isEqualTo("null");
    }

    @Test
    public void canCreateANewSuperHero() throws Exception {
        // when
        MockHttpServletResponse response = mvc.perform(
                post("/superheroes/").contentType(MediaType.APPLICATION_JSON).content(
                        jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
                )).andReturn().getResponse();

        // then
        assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());
    }

    @Test
    public void headerIsPresent() throws Exception {
        // when
        MockHttpServletResponse response = mvc.perform(
                get("/superheroes/2")
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();

        // then
        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getHeaders("X-SUPERHERO-APP")).containsOnly("super-header");
    }
}

The next sections explain this code in detail.

MockitoJUnitRunner and MockMVC

We use the MockitoJUnitRunnerto run our unit test. This one is provided by Mockito and adds some functionality on top of the built-in JUnit runner: detects that the framework is being used, that here are no unused stubs, and initializes for us all the fields annotated with @Mock, so we don’t need to call Mockito.initMocks()method.

Note how we initialize the mocks: our SuperHeroRepository is mocked as usual with the annotation. However, we need it inside our real controller class so we annotate our SuperHeroController instance with @InjectMocks. That way,  the mocked repository is injected into the controller instead of the real bean instance.

For each test, we use our MockMVC instance to perform all kind of fake requests (GET, POST, etc.) and we receive a MockHttpServletResponse in return. Keep in mind that’s not a real response either, everything is being simulated.

JacksonTester initialization

A JacksonTesterobject is also injected here automatically by using the JacksonTester.initFields()method below. This utility class comes with Spring and it’s initialized using a static method as you can see, so it’s kind of tricky.

Configure the Standalone Setup in MockMVC

In the setup method, which is executed before every test, we need to configure MockMVC in Standalone mode and explicitly configure our Controller under test, the Controller Advice and our HTTP Filter. We could add the advice and the filter parts to a base class. But, in any case, you can see already the main disadvantage of this approach: any part of your logic that is modeled within ControllerAdvice’s, Filters, etc., needs to be configured here. This is because you don’t have any Spring context that can inject them automatically.

Testing ControllerAdvices and Filters with MockMVC

Pay attention to how we can verify our surrounding stuff: in line 60 we check that a request with an unexisting id ends up in a NOT_FOUND code, so the ControllerAdvice is working fine. We also have a test method to verify that the header is present, so our Filter is also doing its work. You can do a quick exercise with this code: remove the part of the standalone setup in which we specified the advice and the filter, and run the test again. As expected, it would fail in that case since there is no context to inject these classes.

For the sake of the educational purpose of this post, I included in this test the Filter and the ControllerAdvice. But we could decide not to do that, and leave the tests that verify the presence of the header and the 404 status code to an integration test (so we remove them from here and from the standalone configuration). If we do that we have a pure version of Unit Test: we are testing only the Controller class logic, without any other interference.

Better Assertions with BDDMockito and AssertJ

As a side remark: the code uses BDDMockito and AssertJ to write human-readable, fluent-style tests. If you want to know more about this technique, save this other post for a later read: Write BDD Unit Tests with BDDMockito and AssertJ

Get the book Practical Software Architecture

Strategy 2: MockMVC with WebApplicationContext

Test MockMVC with Context

The second strategy we can use to write Unit Tests for a Controller also involves MockMVC, but in this case we can load a Spring’s WebApplicationContext.  Since we’re still using an inside-server strategy, there is no web server deployed in this case though.

MockMVC and WebMvcTest code example

@RunWith(SpringRunner.class)
@WebMvcTest(SuperHeroController.class)
public class SuperHeroControllerMockMvcWithContextTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private SuperHeroRepository superHeroRepository;

    // This object will be magically initialized by the initFields method below.
    private JacksonTester<SuperHero> jsonSuperHero;

    @Before
    public void setup() {
        // Initializes the JacksonTester
        JacksonTester.initFields(this, new ObjectMapper());
    }

    @Test
    public void canRetrieveByIdWhenExists() throws Exception {
        // given
        given(superHeroRepository.getSuperHero(2))
                .willReturn(new SuperHero("Rob", "Mannon", "RobotMan"));

        // when
        MockHttpServletResponse response = mvc.perform(
                get("/superheroes/2")
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();

        // then
        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getContentAsString()).isEqualTo(
                jsonSuperHero.write(new SuperHero("Rob", "Mannon", "RobotMan")).getJson()
        );
    }

    // ...
    // Rest of the class omitted, it's the same implementation as in Standalone mode

}

When compared with the Standalone mode, these are the main differences:

SpringRunner

The test is executed with SpringRunner, that’s how the context - or part of it - is initialized. When you run the test, you can see at the beginning of the trace how the context starts loading and the injected beans.

MockMVC Autoconfiguration

With the @WebMVCTest annotation our MockMVC instance gets autoconfigured and available in the context (so we can autowire it as you see below in the code). Besides, we specify in the annotation the controller class we want to test. Spring will load then only a partial context (the controller and its surrounding configuration).

The annotation implementation is smart enough to know that our Filter and the Controller Advice should be also injected so, in this case, there is no explicit configuration in the setup() method.

Overriding beans for testing using MockBean

Now the repository is injected in the Spring’s context using @MockBean. We don’t need to make any reference to our controller class apart from the one in the annotation, since the controller will be injected and available. This bean will just replace the real repository implementation.

No server calls

Bear in mind that the responses we’re verifying are still fake. There is no web server involved in this test either. In any case, it’s a perfectly valid test since we’re checking our logic inside our class and, at the same time, some surrounding actors (SuperHeroExceptionHandler and SuperHeroFilter).

Using MockMVC with a Web Application Context - Conclusions

The most important difference is that we didn’t need to explicitly load the surrounding actors since there is a Spring’s context in place. If we create new filters, new controller advices, or any other actor participating in the request-response process, we will get them automatically injected in our test. Therefore, we don’t need to take care of the manual configuration here. There is no fine-grain control over what to use in our test, but it’s closer to what happens in reality. When we run our application, all this stuff is there by default.

You can see here a small transition to Integration Testing. In this case, we’re testing the filter and the controller advice out-of-the-box without making any reference to them. If we would include in the future any other class intervening the request-response flow, it would participate in this test as well.

Since this test includes more than a single class behavior, you could classify it as an Integration Test between those classes. The line is blurry though: on the other hand, you could argue that there is only one Controller under test, but you need the extra configuration to properly test it.

Outside-Server Tests

When you perform an HTTP request to your application to test it, you’re running what I call an outside-server test. However, even being outside you can inject mocks also in these tests, so you could get something similar to a Unit Test also in this case. For instance, in a classical 3-layered application, you could mock the Service layer and test only your Controller through a web server. But, in practice, this approach is much heavier than a normal Unit Test. You’re loading the entire application context, unless you tell Spring not to do so during the test (by excluding configuration or only including what you need).

In Spring you can write outside-server tests for REST controllers using a RestTemplate to perform your requests, or the new TestRestTemplate which includes some useful features for integration testing (ability to include authentication headers and fault tolerance).

In Spring Boot, you can also use the @SpringBootTest annotation. Then you can get, out-of-the-box, some of these beans injected in your context, access to the properties loaded from application.properties, etc. It’s an alternative to @ContextConfiguration (Spring) that gives you all the Spring Boot features for your test.

Testing Strategies in Spring Boot may be confusing given the number of features and available options. Let’s have a look at those strategies, same as we did for MockMVC.

Strategy 3: SpringBootTest with a MOCK WebEnvironment value

If you use @SpringBootTest or @SpringBootTest(webEnvironment = WebEnvironment.MOCK) you don’t load a real HTTP server. Sounds familiar? It’s a similar approach to the strategy 2 (MockMVC with an application context). So, in theory we were intending to use @SpringBootTest annotation to write an outside-server test but, when we set the WebEnvironment to MOCK, we’re converting it to an inside-server test.

We can’t use a RestTemplate since we don’t have any web server, so we need to keep using MockMVC, which now is getting configured thanks to the extra annotation @AutoconfigureMockMVC. This is the trickiest approach between all the available ones in my opinion, and I personally discourage using it.

Better go for Strategy 2 with MockMVC and the context loaded for a specific controller: you’ll be more in control of what you’re testing.

Strategy 4: SpringBootTest with a Real Web Server

Spring Boot Test approach
@SpringBoot test approach

When you use @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) or @SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT) you’re testing with a real HTTP server so, in this case, you need to use a RestTemplate or TestRestTemplate. The difference between using a random port or a defined port is just that in the first case the default port 8080 (or the one you override with the server.port property) won’t be used but replaced with a randomly-assigned port number. This is helpful when you want to run parallel tests, to avoid port clashing. Let’s have a look at the code and then describe the main characteristics.

Spring Boot Test Code Example

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SuperHeroControllerSpringBootTest {

    @MockBean
    private SuperHeroRepository superHeroRepository;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void canRetrieveByIdWhenExists() {
        // given
        given(superHeroRepository.getSuperHero(2))
                .willReturn(new SuperHero("Rob", "Mannon", "RobotMan"));

        // when
        ResponseEntity<SuperHero> superHeroResponse = restTemplate.getForEntity("/superheroes/2", SuperHero.class);

        // then
        assertThat(superHeroResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(superHeroResponse.getBody().equals(new SuperHero("Rob", "Mannon", "RobotMan")));
    }

    @Test
    public void canRetrieveByIdWhenDoesNotExist() {
        // given
        given(superHeroRepository.getSuperHero(2))
                .willThrow(new NonExistingHeroException());

        // when
        ResponseEntity<SuperHero> superHeroResponse = restTemplate.getForEntity("/superheroes/2", SuperHero.class);

        // then
        assertThat(superHeroResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
        assertThat(superHeroResponse.getBody()).isNull();
    }

    @Test
    public void canRetrieveByNameWhenExists() {
        // given
        given(superHeroRepository.getSuperHero("RobotMan"))
                .willReturn(Optional.of(new SuperHero("Rob", "Mannon", "RobotMan")));

        // when
        ResponseEntity<SuperHero> superHeroResponse = restTemplate
                .getForEntity("/superheroes/?name=RobotMan", SuperHero.class);

        // then
        assertThat(superHeroResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(superHeroResponse.getBody().equals(new SuperHero("Rob", "Mannon", "RobotMan")));
    }

    @Test
    public void canRetrieveByNameWhenDoesNotExist() {
        // given
        given(superHeroRepository.getSuperHero("RobotMan"))
                .willReturn(Optional.empty());

        // when
        ResponseEntity<SuperHero> superHeroResponse = restTemplate
                .getForEntity("/superheroes/?name=RobotMan", SuperHero.class);

        // then
        assertThat(superHeroResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(superHeroResponse.getBody()).isNull();
    }

    @Test
    public void canCreateANewSuperHero() {
        // when
        ResponseEntity<SuperHero> superHeroResponse = restTemplate.postForEntity("/superheroes/",
                new SuperHero("Rob", "Mannon", "RobotMan"), SuperHero.class);

        // then
        assertThat(superHeroResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
    }

    @Test
    public void headerIsPresent() throws Exception {
        // when
        ResponseEntity<SuperHero> superHeroResponse = restTemplate.getForEntity("/superheroes/2", SuperHero.class);

        // then
        assertThat(superHeroResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(superHeroResponse.getHeaders().get("X-SUPERHERO-APP")).containsOnly("super-header");
    }

}

We can focus now on the differences.

Web Server Testing

We use the SpringRunner to run our test but we annotate it with @SpringBootTest specifying the RANDOM_PORT mode. Just by doing that, we’ll get a web server up and running for our tests.

Now, we trigger the requests using the template (line 18), same as if we were trying to reach an external server.

The assertions change now a little bit since the response we want to verify is now a ResponseEntity instead of a MockHttpServletResponse.

Mocking layers

Note that we still have the ability to mock the repository layer with @MockBean annotation.

TestRestTemplate

We have a TestRestTemplate bean that we can inject because we’re using @SpringBootTest . It behaves exactly the same as a standard RestTemplate, but has some extra functionalities as seen before.

SpringBootTest approach - Conclusions

Even though our goal is the same - testing the Controller layer, this test approaches the solution from a totally different angle from the one in Strategy 1 (MockMVC in standalone mode). Earlier, we were just loading our class and not even the surrounding actors (filter and controller advice). Now, we’re here loading the entire Spring Boot context with the web server included. This approach is the heaviest of all, and the most further away from the concept of a Unit Test.

This strategy is mainly intended for Integration Tests. The idea is that you can still mock beans and replace those in the context but you can verify interactions between different classes in your Spring Boot application, with the web server participating as well.

My advice is that you should avoid this strategy for Unit Tests. You’re making them fat and you may lose control of what you’re testing. But don’t take me wrong. You should favor this approach for Integration Tests: this testing layer is always useful in your application to verify how the different components work together.

Performance and Context Caching

You may think that Strategy 1 is much more optimal in performance than others. Or that Strategy 4 may perform terribly if you need to load the entire Spring Boot Context every time you run a test. Well, that’s not entirely correct. When you use Spring (including Boot) for testing, the application context will be reused by default during the same Test Suite.

That means, in our case, that Strategies 2, 3 and 4 reuse the Spring context after it loads it for the first time. Be aware that context reuse might cause some side effects if your tests modify beans included in the context. If that’s your case, you’ll need to do some tricks with the @DirtiesContextannotation, indicating that you want to reload the context (see full documentation).

Conclusion

As we saw, you have many alternatives to test unitarily your Controllers in Spring Boot. We covered from the lightest to the heaviest one. It’s time to give you some personal advice about when to use what:

  • Try always to write Unit Tests for your Controller logic without focusing on any other behavior. Go for Strategy 1: use MockMVC in Standalone mode.
  • If you need to test some surrounding behavior related to the Web Layer (such as filtering, interceptors, authentication, etc.), then choose Strategy 4: SpringBootTest with a web server on a random port. However, manage it as a separate Integration Test since you're verifying several parts of your application. Don't skip the Unit Test for the pure Controller layer if you need it. In other words, try to avoid mixing the test layers but rather keep them separate.

I hope you find this guide useful. Let me know if you have any feedback via comments!

Get the book Practical Software Architecture

Now that you are looking into testing controllers maybe you’re also interested in Customizing Error Handling in your REST Controllers, check the new guide for more details.

Looking for a Practical Architecture approach? Then you should consider getting my new mini-book Practical Software Architecture on LeanPub
Moisés Macero's Picture

About Moisés Macero

Software Developer, Architect, and Author.
Do you need help?

Amsterdam, The Netherlands https://thepracticaldeveloper.com