Spring Boot: Integration Tests For REST APIs with Testcontainers, WebFlux and MonogoDb
In this article, you can explore how to implement integration tests for REST APIs using the Java Testcontainers library.
If you haven’t my previous articles, You can find in here: https://medium.com/@ranawaka.y
As a software developer, we should have a basic understanding of why integration testing is essential and the main differences between unit and integration testing.
In simple words, integration testing ensures that the integrated units or modules work correctly from end to end and align with expected requirements, while unit testing verifies that individual components or modules are functioning properly. As an important advantage of integration testing, we can see that it ensures that all modules work as expected when requirements and implementation are changed.
According to https://www.testcontainers.org, Testcontainers for Java 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.
To run an integration test with the testcontainers library, we need Docker and JUnit5 (a supported JVM testing framework).
Testing code is the main section on which this article is focused. Hence, I am going to use a simple REST API that is used to create data in a MongoDB database.
I have the following project structure in my example project:
For the Testcontainers library with JUnit and MongoDB, dependencies are as follows:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.17.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.17.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
<version>1.17.3</version>
<scope>test</scope>
</dependency>
An entity called Notification will be used to insert data into the database.
@Setter
@Getter
@Builder
@Document(collection = "notification")
public class Notification {
@Id
private String id;
private String message;
}
According to the local settings, I have the following properties with the MongoDB database.
spring:
application:
name: demo-service
data:
mongodb:
database: demo
host: localhost
port: 27017
server:
port: 9000
And in the controller, I have implemented simple create and get APIs with ReactiveMongoRepository.
@PostMapping(path = "/create", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<Notification> createNotification(@RequestBody NotificationDto notificationDto) {
return this.notificationService.createNotification(notificationDto);
}
@GetMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<Notification> getNotificationById(@PathVariable(name = "id") String id) {
return this.notificationService.getNotificationById(id);
}
@GetMapping(path = "/all", produces = MediaType.APPLICATION_JSON_VALUE)
public Flux<Notification> getAllNotifications() {
return this.notificationService.getAllNotifications();
}
Now lets start the testing.
As the first step, we can create an abstract class called AbstractIntegrationTest that has a basic implementation.
In this case, we need a container with a mongodb docker image and a WebTestClient interface.
@Autowired
protected WebTestClient webTestClient;
@Container
protected final static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:5.0.12")
.withReuse(true);
Like this, a mongodb container can be initiated, and we can also give a port number and other properties as well. But in this case, we only need properties that should be included in the properties file. For those application Mongodb properties, we can give values using the @DynamicPropertySource annotation.
@DynamicPropertySource
protected static void dynamicPropertiesProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.default.mongodb.host", mongoDBContainer::getHost);
registry.add("spring.data.default.mongodb.database", mongoDBContainer::getHost);
registry.add("spring.data.default.mongodb.port", mongoDBContainer::getHost);
}
Like this, an AbstractIntegrationTest class can be created by giving @Testcontainers and @SpringBootTest annotations.
@Testcontainers
@AutoConfigureWebTestClient(timeout = "3600000")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class AbstractIntegrationTest {
@Autowired
protected WebTestClient webTestClient;
@Container
protected final static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:5.0.12")
.withReuse(true);
static {
mongoDBContainer.start();
}
@DynamicPropertySource
protected static void dynamicPropertiesProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.default.mongodb.host", mongoDBContainer::getHost);
registry.add("spring.data.default.mongodb.database", mongoDBContainer::getHost);
registry.add("spring.data.default.mongodb.port", mongoDBContainer::getHost);
}
}
Now a test class can be implemented for integration testing by extending the AbstractIntegrationTest class.
As I am going to test the create API, the testing process is simple. I just want to post data to the create API and need to check that the response is received as expected.
Therefore, as the first step, the POST API URI and data body should be initiated.
String createUri = "/notifications/create";
String findAllUri = "/notifications/all";
Notification message = Notification.builder()
.message("Test Notification Message")
.build();
Then we can call the URI using the WebTestClient that was injected in the AbstractIntegrationTest class.
webTestClient
.post()
.uri(createUri)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(message), Notification.class)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody()
.jsonPath("$.message").isEqualTo("Test Notification Message");
And if we want, we can call one of the GET APIs and check whether the expected response is received or not. All integration test cases are as:
class NotificationControllerTest extends AbstractIntegrationTest {
@Test
void notificationTest() {
String createUri = "/notifications/create";
String findAllUri = "/notifications/all";
Notification message = Notification.builder()
.message("Test Notification Message")
.build();
webTestClient
.post()
.uri(createUri)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(message), Notification.class)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody()
.jsonPath("$.message").isEqualTo("Test Notification Message");
webTestClient
.get()
.uri(findAllUri)
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectBodyList(Notification.class);
}
}
I hope this will help you to understand and implement integration tests.
The sample source code can be found from here. https://github.com/yasas1/Questions/tree/main/stream-demo/src