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 intended to be used 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.
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
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
.
Server and Client Side Tests
To start with, we can separate server-side and client-side tests. Server-side tests are the most commonly used: you perform your request and you want to check how the server behaves, how the response is formed, what are the retrieved contents, etc.
The client-side tests are not so common, they are useful when you want to verify that an indirect request has been made: 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, that the request has been made and the contents of your request were made, you don’t care about the response since you mocked it. Too bad there are not many good examples of this. Even if you check the official examples, they are not so helpful (see the Javadoc comments). Anyways, 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, the requests are normally mocked, 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 in 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. The first strategy (MockMVC) should be your preferred one if you want to code a real Unit Test, while RestTemplate should be used 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
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.
These are the main parts of this code that we need to focus on:
- We use the
MockitoJUnitRunner
to 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 callMockito.initMocks()
method. - Note how we initialize the mocks: our
SuperHeroRepository
is mocked as usual with the annotation and, since we need it inside our controller class, we annotate ourSuperHeroController
instance with@InjectMocks
, so it’s initialized and the mocked repository is injected in it. - A
JacksonTester
object is also injected here automatically by using theJacksonTester.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. - In the setup method, which is executed before every test, we need to configure Mockito 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 since you don’t have any Spring context that can inject them automatically.
- For each test, we use our
MockMVC
instance to perform all kind of fake requests (GET, POST, etc.) and we receive aMockHttpServletResponse
in return. Keep in mind that’s not a real response either, everything is being simulated. - 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 ourFilter
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.
Strategy 2: MockMVC with WebApplicationContext
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.
When compared with the Standalone mode, these are the main differences:
- 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. - With the
@WebMVCTest
annotation ourMockMVC
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 so only a partial Spring context will be loaded (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 thesetup()
method. - 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. - 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
andSuperHeroFilter
).
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, it will get automatically injected in our test so we don’t need to take care of the manual configuration here. There is no fine-grain control of what to use in our test, but it’s closer to what happens in reality: when we load our application, all this stuff gets automatically injected and configured.
You can see here a small transition to what some people could say it’s an integration test. In this case, we’re testing the filter and the controller advice out-of-the-box without making any reference to them. If we include in the future any other class intervening the request-response flow, it would participate in this test as well. Since more than a class’ behavior is included here, you could classify it as an Integration Testbetween those classes. The line is blurry though: on the other hand, you could argue that there is only one Controller being tested here, but you need the extra configuration provided by those classes to have the full behavior of 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). More specifically, in Spring Boot you can also use the @SpringBootTest
annotation, and then you can get some of these beans injected in your context, access to the properties loaded from application.properties
, etc., out-of-the-box. It’s an alternative to @ContextConfiguration
(Spring) that gives you all the Spring Boot features for your test.
Testing Strategies in Spring Boot can be confusing given the number of features and available shortcuts. Let’s have a look at those strategies 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
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.
Can you spot the differences?
- We use the
SpringRunner
to run our test but we annotate it with@SpringBootTest
specifying theRANDOM_PORT
mode. Just by doing that, we’ll get a web server up and running for our tests. - Note that we’re still mocking the repository layer with
@MockBean
annotation. - We have a
TestRestTemplate
bean that we can inject because we’re using@SpringBootTest
. It behaves exactly the same as a standardRestTemplate
, but has some extra functionalities as seen before. - Our requests are now performed using the template (line 18), same as 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 aMockHttpServletResponse
.
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). While in there we were just loading our class and not even the surrounding actors (filter and controller advice), we’re here loading the entire Spring Boot context with the web server included. This approach is the heaviest of all, and the most distant to 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: don’t use this strategy for Unit Tests, you’re making it fat and you may lose control of what you’re testing unitarily. But don’t take me wrong: favor this approach for Integration Tests and always include that testing layer in your application, verifying how your different components work together.
Performance and Context Caching
You may think that the Strategy 1 is much more optimal in performance than others; and 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 are reusing the Spring context after it’s loaded for the first time.
Be aware that context reuse might cause some side effects if your tests are modifying beans included in the context. If that’s your case, you’ll need to do some tricks with the
@DirtiesContext
annotation and indicate that you want to reload the context.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, so in general 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. But manage it as a separate Integration Test since you’re verifying several parts of your application, and don’t miss the Unit Test for the pure Controller layer if you need it (so try to avoid mixing the test layers and keep them separate).