Skip to content

How to test a controller in Spring Boot – a practical guide

How to test Spring Boot controllers: a practical guide with examples of unit tests and integration tests with and without MockMvc.

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 others are more useful for Integration Tests. In this post, I’ll cover the three main test approaches available for Spring: MockMVC in standalone mode, MockMVC together with SpringRunner, and SpringBootTest.

UPDATED in May 2025. The code examples use Java 24, Spring Boot 3.4, and JUnit 5. If you’re curious about the latest updates, check the Pull Request for the changes.

Introduction

Spring Boot offers several different approaches to testing. It’s a constantly evolving framework, so new versions offer more options while keeping old ones for backward compatibility. This results in multiple ways of testing the same part of our code and some confusion about when to use what. In this post, I’ll help you understand the different alternatives, why they are available, and when to use each.

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 throughout 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 essential to list some particularities of the application to understand further what happens when using the different strategies:

  • If a superhero can’t be found by its identifier, a NonExistingHeroException is thrown. A Spring @RestControllerAdvice intercepts that exception and transforms it into a 404 status code – NOT_FOUND.
  • A SuperHeroFilter class adds a header to the HTTP Response: X-SUPERHERO-APP.

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.

Client-side tests are not so standard, but they are helpful when verifying a request’s composition and actions. In these tests, you mock the server behavior and then call some code (on your side) that will indirectly perform a request to that server. That’s precisely 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. They are not helpful, even if you check the official examples (see the Javadoc comments). Anyway, the essential 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 verify how the server logic works. In this case, you usually mock the requests, and you want to check how your server logic reacts. This kind of test is closely related to the Controller layer in your application since it’s the part of Spring that handles HTTP requests.

Server-Side Tests

If we zoom in on server-side tests, we can identify two main strategies in Spring: writing Controller tests using the MockMVC approach or using RestTemplate. You should favor the first strategy (MockMVC) if you want to code an actual Unit Test, whereas you should use 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 Spring’s WebApplicationContext (partly or wholly, depending on whether it is using the Standalone mode or not). Let’s explain these two strategies in more detail.

Inside-Server Tests

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

Strategy 1: Spring MockMVC example in Standalone Mode

Test MockMVC Standalone diagram
Test MockMVC Standalone

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

MockMVC standalone code example

MockMVC Standalone approach – SuperHeroController

@ExtendWith(MockitoExtension.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;

    @BeforeEach
    public void setup() {
        // We would need this line if we would not use the MockitoExtension
        // MockitoAnnotations.initMocks(this);
        // Here we can't use @AutoConfigureJsonTesters because there isn't a Spring context
        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.getHeader("X-SUPERHERO-APP")).isEqualTo("super-header");
    }
}

Now let’s review the most important parts.

MockitoExtension and MockMVC

We’re using the MockitoExtension annotation for JUnit 5 to enable the use of mocks. (Mockito 2.x.x introduced the capability of JUnit 5 extensions, so we don’t need the initMocks() call anymore).

The MockMvc instance is not being auto-configured, so we are using the MockMvcBuilders class to manually configure it.

JacksonTester initialization

The JacksonTester is a utility class that wraps Jackson functionality to work with JSON objects inside tests. Since there is no Spring Context to autowire it, we need to initialize it through a static method: JacksonTester.initFields().

Configure the Standalone Setup in MockMVC

The standaloneSetup() method allows us to register one or more controllers without the need to use the complete Spring context. This gives us the advantage of loading only the pieces we need.

Since we want to test the exception handling and filter behavior too, we can add them explicitly with .setControllerAdvice(new SuperHeroExceptionHandler()) and .addFilters(new SuperHeroFilter()) respectively.

Testing ControllerAdvice and Filter with MockMVC

Because we added the ControllerAdvice, we get a real 404 status code when the NonExistingHeroException is thrown. The same happens with the filter: we get the special header in the response when we perform our requests.

Better Assertions with BDDMockito and AssertJ

Note that we’re using the given(...).willReturn() statements (BDDMockito) instead of the usual when/then approach. The idea is that we’re using given/when/then inside our tests with a descriptive goal. The approach doesn’t change the functionality.

We’re also using AssertJ instead of the classical JUnit assertions because they offer a more fluent API.

Strategy 2: Spring MockMVC example with WebApplicationContext

Test MockMVC with Context diagram
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 use Spring’s WebApplicationContext. Since we’re still using an inside-server strategy, no web server is deployed in this case.

MockMVC and WebMvcTest code example

@AutoConfigureJsonTesters
@WebMvcTest(SuperHeroController.class)
public class SuperHeroControllerMockMvcWithContextTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private SuperHeroRepository superHeroRepository;

    // This object will be initialized thanks to @AutoConfigureJsonTesters
    @Autowired
    private JacksonTester<SuperHero> jsonSuperHero;

    @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");
    }
}

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

SpringExtension

This time, we need to use a JUnit 5 extension named SpringExtension, provided by Spring. This extension causes the initialization of part of the Spring context. When you run the test, you can see at the beginning of the trace how the context and the injected beans start loading. Note that we don’t need to include this extension explicitly since it’s already included in all the Spring’s @...Test shortcut annotations. See the source code of the WebMvcTest class for an example of how this annotation is included there.

MockMVC Autoconfiguration

With the @WebMVCTest annotation, our MockMVC instance gets auto-configured and made available in the context (so we can autowire it, as you see below in the code). We also specify the controller class we want to test in the annotation. Thanks to this annotation, Spring will load only a partial context (the controller and its surrounding configuration, like Filter and Advice).

Since Spring is smart enough to know that our Filter and the Controller Advice should also be injected, there is no need for explicit configuration in the setup() method.

Overriding beans for testing using MockBean

In this case, we inject a fake repository in the Spring context using the @MockBean annotation. This fake repository bean will replace the actual repository implementation.

We also don’t need to reference our controller class other than the one in the WebMVCTest annotation, since the controller will automatically be injected into the context.

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 related logic inside SuperHeroExceptionHandler and SuperHeroFilter.

Using MockMVC with a Web Application Context – Conclusions

The most important difference of this approach is that we didn’t need to explicitly load the surrounding logic since there is a partial Spring context in place. If we create new filters, new controller Advices, or any other logic participating in the request-response process, we will automatically inject them into our test. Therefore, we don’t need to handle 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 a slight transition to Integration Testing here. In this case, we’re testing the filter and the controller advice out of the box without referencing them. If we include any other class intervening in the request-response flow in the future, it will also participate in this test.

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

Outside-Server Tests

When you perform an HTTP request to your application to test it, you’re running an outside-server test. However, even being outside, you can inject mocks in these tests, so you could get something similar to a Unit Test 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. In practice, this approach is much heavier than a pure Unit Test. Unless you tell Spring not to do so by excluding configuration or only including what you need, you’re loading the entire application context.

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

In Spring Boot, you usually use the @SpringBootTest annotation when creating tests that use the context. Then, you get your beans injected in the context, access to the application properties, etc. It’s a convenient alternative to the classic @ContextConfiguration annotation in Spring that includes all the Spring Boot features for your test.

Given the number of features and available options, testing strategies in Spring Boot may be confusing. Let’s examine them the same way we did for MockMVC.

Strategy 3: SpringBootTest example with a MOCK WebEnvironment value

If you use @SpringBootTest without parameters or with webEnvironment = WebEnvironment.MOCK, you don’t load an actual HTTP server. Does it sound familiar? It’s a similar approach to strategy 2 (MockMVC with an application context). When we use this configuration, we’re still coding an inside-server test.

We can’t use a standard RestTemplate in this setup since we don’t have a web server. Instead, we need to keep using MockMVC, which is now being configured thanks to the extra annotation @AutoconfigureMockMVC. This approach is the trickiest of all the available ones, and I discourage using it. Instead, it’s better to choose Strategy 2 with MockMVC and the context loaded for a specific controller. You’ll be more in control of what you’re testing.

See below the source code changes when we use this strategy.

@AutoConfigureJsonTesters
@SpringBootTest
@AutoConfigureMockMvc
public class SuperHeroControllerSpringBootMockTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private SuperHeroRepository superHeroRepository;

    // This object will be initialized thanks to @AutoConfigureJsonTesters
    @Autowired
    private JacksonTester<SuperHero> jsonSuperHero;

    @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.getHeader("X-SUPERHERO-APP")).isEqualTo("super-header");
    }
}

Strategy 4: SpringBootTest example with a Real Web Server

SpringBoot test approach diagram
SpringBootTest approach

When you use @SpringBootTest with WebEnvironment.RANDOM_PORT or WebEnvironment.DEFINED_PORT), you’re testing with an actual HTTP server. In this case, you must use a RestTemplate or TestRestTemplate to perform the requests.

The difference between using a random or defined port is that in the first case, the default port 8080 (or the one you override with the server.port property) won’t be used but will be replaced with a randomly assigned port number. This is helpful when running parallel tests to avoid port clashing. Let’s look at the code and then describe the main characteristics.

Spring Boot Test Code Example

@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()).isEqualTo(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

Now, we annotate our test with @SpringBootTest, and a RANDOM_PORT web-environment mode. By doing that, we’ll get a web server up and running for our tests.

In this case, we trigger the requests using a REST template (line 18); more specifically, its TestRestTemplate extension, as if we were trying to reach an external server.

The assertions have changed a bit since the response we want to verify is now a ResponseEntity instead of a MockHttpServletResponse. However, they’re very similar.

Mocking layers

Note that we can still mock the repository layer with the @MockBean annotation.

TestRestTemplate

We have a TestRestTemplate bean in the context because we’re using the @SpringBootTest annotation. This test version behaves precisely 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 different angle than the one in Strategy 1 (MockMVC in standalone mode). Earlier, we were loading our class and not even the surrounding actors (filter and controller advice). Here, we’re loading the entire Spring Boot context with the web server included. This approach is the heaviest of all and the furthest away from the concept of a Unit Test.

This strategy is mainly intended for Integration Tests. You can still mock beans and replace them in the context. You can also verify interactions between different classes in your Spring Boot application with a web server that participates in the process.

I would avoid using this strategy for Unit Tests. You’re building fat tests and may also lose control of what you’re testing. However, you should favor this approach for Integration Tests. These tests are very useful in verifying how the different components work together in your application.

Performance and Context Caching

You may think Strategy 1 is much more optimal in performance than the others. Or 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.

In our case, Strategies 2, 3, and 4 reuse the Spring context after it’s loaded for the first time. However, remember that reusing the context might cause side effects if your tests modify Spring beans. If that’s your case, you’ll need to do some tricks with the @DirtiesContext, indicating that you want to reload the context before each test (see the docs if you’re curious).

Conclusion

As we saw, we have many alternatives to build Unit Tests for our Controllers in Spring Boot. We reviewed from the lightest to the heaviest one. It’s time to give you some recommendations about when to use what:

  • Always write Unit Tests for your Controller logic without focusing on other behaviors. Choose 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.), choose Strategy 4: SpringBootTest with a web server on a random port. However, treat it as an Integration Test since you’re verifying several parts of your application. Don’t skip the Unit Test for the pure Controller layer if needed. In other words, try to avoid mixing the test layers but keep them separate.

I hope you find this guide valuable. Please let me know if you have any feedback via comments! If you want to learn more about Spring Boot and how to build microservices from scratch, check out my book.

Master Microservices with Spring Boot

A hands-on guide to building production-ready microservices

Get the Book

Get More Architecture Insights

Join hundreds of software architects and engineers getting practical insights delivered to their inbox.

Back to Blog