Spring Boot MongoDB - Integration Testing with Testcontainers
In this section, we will learn how to test Spring Boot, Spring Data MongoDB, and MongoDB based application using Testcontainers and @SpringBootTest.
1. What we will build?
We will create a web application with Spring Boot, Spring Data MongoDB and MongoDB. The application will consist of three layer, that is a Controller, a Service, and a Repository layer.
- Controller layer takes care of mapping request data to the defined request handler method. Once response body is generated from the handler method, it converts it to JSON response.
- Service layer facilitates communication between the controller and the repository layer.
- We are using Spring Data MongoDB for managing database operations, so we can use Spring Data MongoRepository interface.
**Finally we will do a integration testing with help of Testcontainers to verify our system is working as expected.
2. Testcontainers
Testcontainers is an open source testing library that allows us to run docker containers directly in our spring boot application in order to facilitate integration tests with real dependencies.
It can provide instances of common databases(here MongoDB), message brokers, Selenium web browsers, or anything else that can run in a Docker container.
3. Install docker
Install docker in your local machine if you not yet installed docker. Go to docker official https://docs.docker.com/engine/install/#desktop and download docker and then install.
4. Creating a spring boot application
First, open the Spring initializr https://start.spring.io
Then, Provide the Group and Artifact name. We have provided Group name com.knf.dev.demo and Artifact spring-mongodb-testcontainers-integration-testing. Here I selected the Maven project - language Java 17 - Spring Boot 3.1.6, Spring Web, Testcontainers, and Spring Data MongoDB.
Then, click on the Generate button. When we click on the Generate button, it starts packing the project in a .zip(spring-mongodb-testcontainers-integration-testing) file and downloads the project. Then, Extract the Zip file.
Then, import the project on your favourite IDE.
Final Project directory:
Complete pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>spring-mongodb-testcontainers-integration-testing</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-mongodb-testcontainers-integration-testing</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder-jammy-base:latest</builder>
</image>
</configuration>
</plugin>
</plugins>
</build>
</project>
spring-boot-testcontainers:By using this dependency spring automatically configures the necessary Spring Boot properties for the supporting containers.
junit-jupiter testcontainers extension which will take care of starting and stopping of the containers.
spring-boot-starter-test starter will provide following libraries:
- JUnit
- Spring Test & Spring Boot Test
- AssertJ
- Hamcrest
- Mockito
- JSONassert
- JsonPath
Spring Data MongoDB– Student Document
package com.knf.dev.demo.document;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "students")
public class Student {
@Id
private String id;
private String name;
private String email;
private Integer age;
public Student() {
}
public Student(String id, String name, String email, Integer age) {
this.id = id;
this.name = name;
this.email = email;
this.age = age;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
- @Document is used to map a class to mongoDB database, it represents a MongoDB documents.
Spring Data MongoDB– Student Repository
package com.knf.dev.demo.repository;
import com.knf.dev.demo.document.Student;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import java.util.List;
import java.util.Optional;
public interface StudentRepository extends MongoRepository<Student, String> {
@Query("{name : ?0}")
Optional<Student> findByName(String name);
@Query("{ age : { $gte: ?0 } }")
List<Student> findByAgeGreaterThan(Integer age);
List<Student> findByAgeLessThan(Integer age);
}
- findByAgeGreaterThan(): If we want to retrieve students whose age is greater than the given age.
- findByName(): This method will get student document by name.
- findByAgeLessThan(): If we want to retrieve students whose age is less than the given age.
- MongoRepository provides all the necessary methods which help to create a CRUD application and it also supports the custom derived query methods.
Create StudentService.java
package com.knf.dev.demo.service;
import com.knf.dev.demo.document.Student;
import java.util.List;
import java.util.Optional;
public interface StudentService {
List<Student> findStudentByAgeGreaterThan(Integer age);
Optional<Student> findByName(String name);
List<Student> findByAgeLessThan(Integer age);
void saveAllStudent(List<Student> students);
}
Create StudentServiceImpl.java
package com.knf.dev.demo.service;
import com.knf.dev.demo.document.Student;
import com.knf.dev.demo.repository.StudentRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class StudentServiceImpl implements StudentService{
private final StudentRepository studentRepository;
public StudentServiceImpl(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
@Override
public List<Student> findStudentByAgeGreaterThan(Integer age) {
return studentRepository.findByAgeGreaterThan(age);
}
@Override
public Optional<Student> findByName(String name) {
return studentRepository.findByName(name);
}
@Override
public List<Student> findByAgeLessThan(Integer age) {
return studentRepository.findByAgeLessThan(age);
}
@Override
public void saveAllStudent(List<Student> students) {
studentRepository.saveAll(students);
}
}
Create Student Controller
package com.knf.dev.demo.controller;
import com.knf.dev.demo.document.Student;
import com.knf.dev.demo.service.StudentService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/v1")
public class StudentController {
private final StudentService studentService;
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
@GetMapping("/students/agt/{age}")
public List<Student> findStudentByAgeGreaterThan(@PathVariable Integer age)
{
return studentService.findStudentByAgeGreaterThan(age);
}
@GetMapping("/students/alt/{age}")
public List<Student> findStudentByAgeLessThan(@PathVariable Integer age)
{
return studentService.findByAgeLessThan(age);
}
@GetMapping("/students/{name}")
public Student findStudentByName(@PathVariable String name)
{
return studentService.findByName(name).get();
}
}
The @RestController annotation is mainly utilized for building restful web services utilizing Spring MVC. It is a convenience annotation, this annotation itself annotated with @ResponseBody and @Controller annotation. The class annotated with @RestController annotation returns JSON replication in all the methods.
@RequestMapping is used to map web requests onto specific handler classes and/or handler methods. @RequestMapping can be applied to the controller class as well as methods.
@GetMapping annotation for mapping HTTP GET requests onto specific handler methods.
Application.java
package com.knf.dev.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Implementing the Tests
When using JUnit 4, @SpringBootTest annotation should be used in combination with @RunWith(SpringRunner.class). But for this example we are using JUnit 5, there’s no need to add the equivalent @ExtendWith(SpringExtension.class).
package com.knf.dev.demo;
import com.knf.dev.demo.document.Student;
import com.knf.dev.demo.repository.StudentRepository;
import com.knf.dev.demo.service.StudentService;
import org.junit.jupiter.api.BeforeAll;
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.server.LocalServerPort;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public class StudentApplicationIntegrationTests {
@Autowired
private StudentService studentService;
@LocalServerPort
private Integer port;
@Container
@ServiceConnection
public static MongoDBContainer mongoDBContainer =
new MongoDBContainer(DockerImageName.parse("mongo:latest"));
//Method should be executed before all tests in the current test class
//Load initial data
@BeforeAll
static void setup(@Autowired StudentRepository studentRepository) {
Student student1 = new Student("101","Alpha","alpha@knf.com",50);
Student student2 = new Student("102","Beta","beta@knf.com",40);
Student student3 = new Student("103","Gama","gama@knf.com",30);
Student student4 = new Student("104","Pekka","pekka@knf.com",20);
List<Student> students = Arrays.asList(student1,student2,student3,student4);
studentRepository.saveAll(students);
}
@Test
void findByName_ReturnsTheStudent() {
final String name ="Alpha";
RestTemplate restTemplate = new RestTemplate();
String resourceUrl= "http://localhost:"+port+"/api/v1/students/{name}";
// Fetch response as List wrapped in ResponseEntity
ResponseEntity<Student> findByName = restTemplate.exchange(
resourceUrl,
HttpMethod.GET,
null,
new ParameterizedTypeReference<Student>(){},name);
Student student = findByName.getBody();
assertThat(student).isNotNull();
assertThat(student.getEmail()).isEqualTo("alpha@knf.com");
assertThat(student.getName()).isEqualTo("Alpha");
assertThat(student.getId()).isEqualTo("101");
assertThat(student.getAge()).isEqualTo(50);
}
@Test
void findByAgeGreaterThan_ReturnsTheListStudents() {
final Integer age = 29;
RestTemplate restTemplate = new RestTemplate();
String resourceUrl= "http://localhost:"+port+"/api/v1/students/agt/{age}";
// Fetch response as List wrapped in ResponseEntity
ResponseEntity<List<Student>> findByAgeGreaterThan = restTemplate.exchange(
resourceUrl,
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Student>>(){},age);
List<Student> students = findByAgeGreaterThan.getBody();
//Convert list of students to list of id(String)
List<String> ids = students.stream()
.map(o -> o.getId())
.collect(Collectors.toList());
assertThat(students.size()).isEqualTo(3);
assertThat(ids).hasSameElementsAs(Arrays.asList("103","102","101"));
}
@Test
void findByAgeLessThan_ReturnsTheListStudents() {
final Integer age = 31;
RestTemplate restTemplate = new RestTemplate();
String resourceUrl= "http://localhost:"+port+"/api/v1/students/alt/{age}";
// Fetch response as List wrapped in ResponseEntity
ResponseEntity<List<Student>> findByAgeLessThan = restTemplate.exchange(
resourceUrl,
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Student>>(){},age);
List<Student> students = findByAgeLessThan.getBody();
//Convert list of students to list of id(Integer)
List<String> ids = students.stream()
.map(o -> o.getId())
.collect(Collectors.toList());
assertThat(students.size()).isEqualTo(2);
assertThat(ids).hasSameElementsAs(Arrays.asList("104","103"));
}
}
- @SpringBootTest can be used to load complete application context for end to end integration testing.
- webEnvironment=SpringBootTestWebEnvironment.RANDOM_PORT: Loads WebApplicationContext and provides a real web environment. Embedded servers start and listen on a random port.
- @Testcontainers is a JUnit Jupiter extension which automatically starts and stops the containers(here MongoDBContainer) that are used in the tests.
- To get the port number on which your Spring Boot application is running, you can use the @LocalServerPort annotation.
- The @Container annotation is a JUnit extension that tells JUnit to notify this field about various events in a test lifecycle. In this case, it ensures that the MongoDB container is running in a healthy way before any test is considered successfully run.
- Beginning from Spring Boot version 3.1, the annotation @ServiceConnection can be used on the container instance fields of our tests. We are using @ServiceConnection instead of @DynamicPropertySource to register the dynamic property values to a MongoDB container.
- Testcontainers has a MongoDBContainer class that enables you to create a MongoDB instance for testing purposes.
- assertThat is used to check the specified value matches the expected value. It will accept the two parameters, the first contains the actual value, and the second will have the object matching the condition.
5. Run the test
Since we are using Testcontainers, make sure to start Docker in your local machine.
After that, run the test,
Or you can run the test using following command:
mvn test -Dtest=StudentApplicationIntegrationTests
or
mvn test