The best way to confirm the reliability and correctness of software applications is to write tests that assert the desired behavior of an application. This post covers how to perform unit and integration testing in Spring Boot.
The spring-boot-starter-test
starter dependency is required for writing the unit and integration tests in Spring Boot application. The Spring Boot version 3.2.2 spring-boot-starter-test
includes the following transitive dependencies.
- Jupiter JUnit5: The de facto standard for unit testing Java.
- Jayway JsonPath: A popular Java library used for parsing and querying JSON documents. It is used for navigating through the structure of your JSON data and access specific values or elements.
- Awaitility: Used for waiting for asynchronous operations to complete without introducing additional logic in your code.
- Hamcrest: Hamcrest is used for writing expressive and readable assertions in unit tests. It provides a clear and concise way to express what you expect from your code, making your tests easier to understand and maintain.
- Mockito: Java mocking framework used to create mock objects that simulate the behavior of real objects without actually implementing their functionality. This makes it easier to isolate the code you're testing from external dependencies and write more focused and reliable tests.
Let’s start by creating a new Spring application. You can create a Spring boot project either using Spring CLI or using Spring Initializr.
When we create a new Spring boot project the spring-boot-starter-test
dependencies is added by default. If you don't have it already, you can add this manually in your pom.xml
or build.gradle
file.
For maven, add the following to the <dependencies>
section in your pom.xml
file
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
The test
scope defines that the spring-boot-starter-test
dependency is required during the test compilation and execution phases.
For gradle,
testImplementation 'org.springframework.boot:spring-boot-starter-test'
The Spring Initializr also includes a default test class in the root directory of the test package.
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class MoviesApplicationTests {
@Test
void contextLoads() {
}
}
The @SpringBootTest
annotation tells Spring Boot to look for a main configuration class (one with @SpringBootApplication
, for instance) and use that to start the Spring application context.
The @Test
annotation a JUnit annotation that will execute the method when the tests starts. A test class can contain one or more test methods. When there are multiple test methods, the order of execution is not fixed. If you want to fix the execution order you can do that using the method name, display name, using order annotation. Checkout this tutorials that explains how to order test methods.
You can run this test in your IDE or on the command line using following Maven or Gradle commands.
./mvnw test
or
./gradlew test
Simple Test
Now that we have the required test dependency in our project and we have learnt how to execute the test, let us now test the following controller.
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
@RequestMapping("/")
public String greeting() {
return "Hello, Spring Boot!";
}
}
Let us now verify if the Spring context is creating the instance of GreetingsController
with an assertion.
import com.stacktips.movies.api.GreetingsController;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
@SpringBootTest
class MoviesApplicationTests {
@Autowired
private GreetingController controller;
@Test
void contextLoads() {
assertThat(controller, is(notNullValue()));
}
}
The @Autowired
injects the controller instance before the test methods are run. We have hamcrest which provides assertThat()
method to assert the not null value.
[!TIP] The Spring Test support caches the application context between tests, so that if you have multiple methods in a test case or multiple test cases with the same configuration, they incur the cost of starting the application only once. You can control the cache by using the
@DirtiesContext
annotation.
Testing the REST API
Let us now write some tests to assert the behavior of your application. For that we will start the application and and listen for a connection (as it would do in production) and then send an HTTP request and assert the response.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class GreetingsControllerTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void greetingShouldReturnDefaultMessage() throws Exception {
String result = restTemplate
.getForObject("http://localhost:" + port + "/", String.class);
assertThat(result, is("Hello, Spring Boot!"));
}
}
The webEnvironment=RANDOM_PORT
to start the server with a random port. This is very useful to avoid conflicts in test environments and the injection of the port with @LocalServerPort
.
Spring boot test also provides a TestRestTemplate
for to make http calls from your test.
Using MockMvc
In the above approach we have started the server but if we want to tests to only the web layer by without starting a server we can do that using @WebMvcTest
. For that, we will inject an instance of MockMvc
. For MockMvc to work we need to need to use the @AutoConfigureMockMvc
annotation on our test class.
If we have multiple controllers, we can instantiate specific ones by using the@WebMvcTest(HomeController.class)
annotation on the class.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
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.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(GreetingController.class)
class GreetingControllerTest {
@Autowired
MockMvc mockMvc;
@Test
void greetingShouldReturnDefaultMessage() throws Exception {
this.mockMvc.perform(get("/"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string(containsString("Hello, Spring Boot!")));
}
}
The test assertion is the same as in the previous case. However, in this test, Spring Boot instantiates only the web layer rather than using the whole spring context.
Mocking Services
So far, our HomeController
is simple and has no dependencies. But we will often have additional services to isolate the business logic into separate classes. For example.
MovieService
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MovieService {
public List<String> getMovies() {
return List.of("The Incredibles", "Father of the Bride", "The Parent Trap");
}
}
And, lets say our MovieController has /movies
GET endpoint.
import com.stacktips.movies.service.MovieService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class MovieController {
private final MovieService movieService;
public MovieController(MovieService movieService) {
this.movieService = movieService;
}
@RequestMapping("/movies")
public List<String> getMovies() {
return movieService.getMovies();
}
}
In the above code snippet, Spring automatically injects the MovieService
dependency into the controller as we have only one constructor defined. If you run the spring boot application and test /movies
endpoint, it will return the list of movies.
nilan > curl http://localhost:8080/movies
["The Incredibles","Father of the Bride","The Parent Trap"]
Let us now create a test for the MoviesController
and mock the service instance. To mock the Spring bean the @MockBean
annotation is used.
import com.stacktips.movies.service.MovieService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import static org.hamcrest.CoreMatchers.is;
import static org.mockito.Mockito.when;
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;
@WebMvcTest(MovieController.class)
class MovieControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private MovieService movieService;
@Test
void greetingShouldReturnMockResponse() throws Exception {
List<String> moviesMock = List.of("Sprider Man", "X-Man", "Iron Man");
when(movieService.getMovies()).thenReturn(moviesMock);
this.mockMvc.perform(get("/movies"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$[0]", is("Sprider Man")))
.andExpect(jsonPath("$[1]", is("X-Man")))
.andExpect(jsonPath("$[2]", is("Iron Man")));
}
}
The mockMvc.perform
method makes the GET request to the /movies
endpoint. But this time, instead of getting the results from the service, it returns the mocked response using Mockito
.
We are also using the Json path to assert the Json response.