In this tutorial, we will learn how to do Spring Boot application Integration Testing using Testcontainers.
First, we write Integration tests using a local MySQL database, and then we will address the problem with Testcontainers as a solution.
SpringBoot provides excellent support for integration testing using @SpringBootTest annotation. We can use @SpringBootTest annotation to load the application context and test various components.
@SpringBootTest will bootstrap the full application context, which means we can @Autowire any bean that's picked up by component scanning into our test.
In this tutorial, we are going to use @SpringBootTest annotation for Integration testing.
As the name suggests, integration tests focus on integrating different layers of the application. That also means no mocking is involved.
Basically, we write integration tests for testing a feature that may involve interaction with multiple components.
Examples: Integration testing of complete Employee Management Feature ( EmployeeRepository, EmployeeService, EmployeeController).
Integration testing of complete User Management Feature (UserController, UserService, and UserRepository).
Integration testing of complete Login Feature (LoginRespository, LoginController, Login Service), etc
Using spring initialize, create a Spring Boot project and add the following dependencies:
Generate the Spring boot project as a zip file, extract it, and import it into IntelliJ IDEA.
Let's use the MySQL database to store and retrieve the data in this example and we gonna use Hibernate properties to create and drop tables.
Open the application.properties file and add the following configuration to it:
spring.datasource.url=jdbc:mysql://localhost:3306/demo?useSSL=false spring.datasource.username=root spring.datasource.password=Mysql@123 spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect spring.jpa.hibernate.ddl-auto = create-drop
Make sure that you will create a demo database before running the Spring boot application. Also, change the MySQL username and password as per your MySQL installation on your machine.
Next, let's create a Student JPA entity:
package net.javaguides.spirngboot.entity; import lombok.*; import javax.persistence.*; @Setter @Getter @Builder @AllArgsConstructor @NoArgsConstructor @Entity @Table(name = "students") public class Student { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(name = "first_name") private String firstName; @Column(name = "last_name") private String lastName; private String email; }
Let's create StudentRepository which extends the JpaRepository interface:
package net.javaguides.spirngboot.repository; import net.javaguides.spirngboot.entity.Student; import org.springframework.data.jpa.repository.JpaRepository; public interface StudentRepository extends JpaRepository{ }
Let's create StudentController class and add these couple of REST endpoints:
package net.javaguides.spirngboot.controller; import net.javaguides.spirngboot.entity.Student; import net.javaguides.spirngboot.repository.StudentRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/students") public class StudentController { @Autowired private StudentRepository studentRepository; @PostMapping @ResponseStatus(HttpStatus.CREATED) public Student createStudent(@RequestBody Student student){ return studentRepository.save(student); } @GetMapping public ListgetAllStudents(){ return studentRepository.findAll(); } }
Now, let's create an Integration JUnit test for GET ALL Students REST API:
package net.javaguides.spirngboot; import net.javaguides.spirngboot.entity.Student; import net.javaguides.spirngboot.repository.StudentRepository; import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import java.util.List; @SpringBootTest @AutoConfigureMockMvc class SpringbootTestcontainersDemoApplicationTests { @Autowired private StudentRepository studentRepository; @Autowired private MockMvc mockMvc; // given/when/then format - BDD style @Test public void givenStudents_whenGetAllStudents_thenListOfStudents() throws Exception { // given - setup or precondition List<Student> students = List.of(Student.builder().firstName("Ramesh").lastName("faadatare").email("ramesh@gmail.com").build(), Student.builder().firstName("tony").lastName("stark").email("tony@gmail.com").build()); studentRepository.saveAll(students); // when - action ResultActions response = mockMvc.perform(MockMvcRequestBuilders.get("/api/students")); // then - verify the output response.andExpect(MockMvcResultMatchers.status().isOk()); response.andExpect(MockMvcResultMatchers.jsonPath("$.size()", CoreMatchers.is(students.size()))); } }
Let's understand the above code.
We are using @SpringBootTest annotation to load the application context and test various components. MockMvc provides support for Spring MVC testing. It encapsulates all web application beans and makes them available for testing. @AutoConfigureMockMvc annotation that can be applied to a test class to enable and configure auto-configuration of MockMvc.
@Autowired private MockMvc mockMvc;
The MockMvc.perform() method will call a GET request method, which returns the ResultActions.
ResultActions response = mockMvc.perform(MockMvcRequestBuilders.get("/api/students"));
Using this result, we can have assertion expectations about the response, like its content, HTTP status, or header.
The andExpect() will expect the provided argument. In our case, we're expecting HTTP status code and the size of the JSON array in the response:
// then - verify the output response.andExpect(MockMvcResultMatchers.status().isOk()); response.andExpect(MockMvcResultMatchers.jsonPath("$.size()", CoreMatchers.is(students.size())));
A common problem when writing integration tests is the dependency on installed components (Ex: MySQL, RabbitMQ) where the integration tests are supposed to run.
In our case, our Integration tests depend on the MySQL database. Installing a specific version of the MySQL database in every machine where the integration tests are supposed to run takes a lot of time right.
Basically, our integration tests depend on external services (installing MySQL, Rabbit MQ, Redis, etc) to run the integration tests right then how to reduce this dependency - what will be the solution.
The solution is Testcontainers.
Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.
Using Testcontainers is fairly easy and it gives us the opportunity to create integration tests without the need for pre-installed components.
Using Testcontainers, we would always start with a clean database and our integration tests could run on any machine.
Testcontainer allows us to use Docker containers within our tests. As a result, we can write self-contained integration tests that depend on external resources.
Open the pom.xml file and add the following Testcontainers dependencies:
<dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <version>1.16.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <version>1.16.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>mysql</artifactId> <version>1.16.2</version> <scope>test</scope> </dependency>
Let's change the Integration test to use Testcontainers.
We gonna use the Singleton containers pattern to use Testcontainers.
Singleton containers pattern is useful to define a container that is only started once for several test classes.
Let us create a base class AbstractContainerBaseTest so that all our integration tests can extend without repeating the common configuration.
package net.javaguides.spirngboot; import org.testcontainers.containers.MySQLContainer; public class AbstractContainerBaseTest { static final MySQLContainer MY_SQL_CONTAINER; static { MY_SQL_CONTAINER = new MySQLContainer("mysql:latest"); MY_SQL_CONTAINER.start(); } }
Now, simply extend our Integration class with the above AbstractContainerBaseTest:
package net.javaguides.spirngboot; import net.javaguides.spirngboot.entity.Student; import net.javaguides.spirngboot.repository.StudentRepository; import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import java.util.List; @SpringBootTest @AutoConfigureMockMvc class SpringbootTestcontainersDemoApplicationTests extends AbstractContainerBaseTest{ @Autowired private StudentRepository studentRepository; @Autowired private MockMvc mockMvc; // given/when/then format - BDD style @Test public void givenStudents_whenGetAllStudents_thenListOfStudents() throws Exception { // given - setup or precondition List<Student> students = List.of(Student.builder().firstName("Ramesh").lastName("faadatare").email("ramesh@gmail.com").build(), Student.builder().firstName("tony").lastName("stark").email("tony@gmail.com").build()); studentRepository.saveAll(students); // when - action ResultActions response = mockMvc.perform(MockMvcRequestBuilders.get("/api/students")); // then - verify the output response.andExpect(MockMvcResultMatchers.status().isOk()); response.andExpect(MockMvcResultMatchers.jsonPath("$.size()", CoreMatchers.is(students.size()))); } }
Before running the Integration test make sure that Docker is running on your machine otherwise, you won't be able to run the Integration test.
Here is the output of the above Integration test using Testcontainers:
In this tutorial, we have discussed how to perform Spring Boot application Integration Testing using Testcontainers.