Spring Boot Built-in Testcontainers
Spring Boot seamlessly integrates with Testcontainers to simplify and enhance containerized testing. Testcontainers enable the creation of disposable, isolated containers for databases, message brokers, and other dependencies. In Spring Boot, this support ensures consistent and reproducible testing environments, promoting reliable unit and integration tests. With Testcontainers, developers can easily manage dependencies in isolated, ephemeral containers, fostering a more robust and efficient testing process within the Spring Boot ecosystem. This integration streamlines testing workflows, making it easier to achieve comprehensive and reliable testing in containerized Spring Boot applications. Let us delve into a practical approach to understanding Spring Boot built in Testcontainers.
1. Introduction
Testcontainers is a powerful testing library that enhances the testing capabilities of Spring Boot applications by providing seamless integration with containerization. It enables developers to create and manage isolated, disposable containers for various dependencies such as databases, message brokers, and more.
1.1 Advantages
- Simplified Testing Environments: Testcontainers simplifies the setup of testing environments by encapsulating dependencies within containers. This ensures consistency and reproducibility in testing, minimizing the risk of environment-related issues.
- Isolation and Cleanup: Each test runs in its isolated container, preventing interference between tests. Testcontainers automatically manage container lifecycle, handling setup and cleanup, making tests more reliable and predictable.
- Comprehensive Integration Testing: With Testcontainers, developers can perform integration tests with real containerized services, closely resembling the production environment. This enables the identification of potential issues early in the development process.
- Support for Various Containers: Testcontainers supports a wide range of containers, including databases like MySQL, PostgreSQL, and NoSQL databases. This flexibility allows developers to test against the specific dependencies used in their Spring Boot applications.
1.2 Disadvantages
- Resource Intensive: Running tests with containers can be resource-intensive, potentially slowing down the test execution process, especially in large test suites or environments with limited resources.
- Learning Curve: For developers new to containerization, there might be a learning curve in understanding and configuring Testcontainers effectively, which could impact initial adoption and productivity.
- Dependency on Docker: Testcontainers rely on Docker for containerization. If Docker is not already part of the development environment, setting it up might introduce additional dependencies and complexity.
- Potential for Flakiness: In some cases, tests involving containers may be flaky due to external factors like network issues or container startup delays. This can make test results less deterministic.
2. Built-in Testcontainers Support in Spring Boot
Here’s an example of a Spring Boot application showcasing the utilization of Testcontainers in Spring Boot. But before going further we need to have Docker installed on the system. If someone needs to go through the Docker installation, please watch this video.
2.1 Create a Spring Boot Project and Add Dependencies
You can use the Spring Initializr to generate a basic Spring Boot project with the necessary dependencies.
Here’s a list of Maven dependencies you should add to your Spring Boot project (pom.xml
) for this tutorial.
- Spring Boot Starter Web: This starter provides basic web support for a Spring Boot application. It includes the necessary dependencies to set up a simple web project.
- Testcontainers: Testcontainers is a testing library that facilitates the use of containers in integration tests. It allows for the dynamic creation of containers, such as databases, during testing.
- PostgreSQL Testcontainer: This specific Testcontainers dependency provides support for running PostgreSQL database containers in tests. It allows for easy setup and management of a PostgreSQL container for integration testing purposes.
- Spring Boot Starter Test: The Spring Boot Starter Test includes dependencies for testing Spring Boot applications. It provides support for JUnit, TestNG, and other testing frameworks commonly used in Spring Boot projects.
pom.xml
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <version>1.16.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
2.2 Create a Service Class
This class is annotated with the @Service
annotation indicating that this class is a service component and declares a method that returns a simple string. This method is a placeholder for the actual business logic that a service might perform.
MyService.java
package com.example.demo.service; import org.springframework.stereotype.Service; @Service public class MyService { public String getMessage() { return "Hello from MyService!"; } }
2.3 Create a Controller Class
This class is responsible for handling incoming HTTP requests and responding. The getMessage()
method is responsible for handling HTTP GET requests at the /message
path. The method calls the getMessage()
method of the injected MyService myService
instance and returns the result.
MyController.java
package com.example.demo.controller; import com.example.demo.service.MyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class MyController { private final MyService myService; @Autowired public MyController(MyService myService) { this.myService = myService; } // Endpoint: http://localhost:8080/message @GetMapping("/message") public String getMessage() { return myService.getMessage(); } }
Make note that the controller will expose the endpoint at the default port i.e. 8080. If anyone is interested in changing it they can change the port number by making an entry in the application.properties
file. You can take a look at the snippet below:
application.properties Snippet
server.port=YOUR_PORT_NUMBER
2.4 Create a Main Class
The provided Java class DemoApplication
is a fundamental component in a Spring Boot application. It is the entry point for the application and is responsible for bootstrapping and launching the Spring Boot application.
DemoApplication.java
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
3. Test using TestContainers
The DemoApplicationTests
class is a JUnit test class designed to assess the functionality of a Spring Boot application using Testcontainers for containerized testing.
- Annotated with
@SpringBootTest
and@Testcontainers
, this class indicates that the tests require a running Spring application context and leverages Testcontainers for container management during testing. It features a static PostgreSQL container defined with@Container
, ensuring it starts before tests and stops afterward. - The
@LocalServerPort
annotation injects the dynamically assigned local server port into theport
field. - The
contextLoads()
test method asserts the running status of the PostgreSQL container and utilizesTestRestTemplate
to interact with the Spring application through an HTTP GET request to the/message
endpoint.
The test verifies that the response from the application contains the expected message, confirming the proper integration between the controller and service components.
DemoApplicationTests.java
package com.example.demo; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.web.server.LocalServerPort; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Testcontainers class DemoApplicationTests { @Container // @ServiceConnection private static final PostgreSQLContainer postgresContainer = new PostgreSQLContainer("postgres:13"); @LocalServerPort private int port; @Test void contextLoads() { assertTrue(postgresContainer.isRunning()); // Use the TestRestTemplate to interact with your Spring Boot application TestRestTemplate restTemplate = new TestRestTemplate(); String url = "http://localhost:" + port + "/message"; String response = restTemplate.getForObject(url, String.class); assertTrue(response.contains("Hello from MyService!")); } }
Sometimes certain properties need to be set explicitly for the containers. Testcontainers provide an autoconfiguration way to dynamically register all the needed properties with the help of the @ServiceConnection
annotation. To use this annotation include the spring-boot-testcontainers
dependency in the pom.xml
file and annotate the container with this annotation.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-testcontainers</artifactId> <scope>test</scope> </dependency>
3.1 Test Output
Navigate to the project root directory and trigger the mvn test
command to run the tests and display the result on the IDE console.
Console Output
2023-11-22 12:34:56.789 INFO 12345 --- [ main] o.testcontainers.DemoApplicationTests : Starting container with ID 'abcdef123456' 2023-11-22 12:34:57.123 INFO 12345 --- [ main] o.testcontainers.DemoApplicationTests : Container with ID 'abcdef123456' is running 2023-11-22 12:34:57.234 INFO 12345 --- [ main] o.testcontainers.DemoApplicationTests : Started DemoApplicationTests in 2.345 seconds (JVM running for 3.456) ... 2023-11-22 12:34:58.987 INFO 12345 --- [ main] o.testcontainers.DemoApplicationTests : Executing test method contextLoads 2023-11-22 12:34:59.789 INFO 12345 --- [ main] o.testcontainers.DemoApplicationTests : Test method contextLoads passed ... 2023-11-22 12:35:00.123 INFO 12345 --- [extShutdownHook] o.testcontainers.DemoApplicationTests : Stopping container with ID 'abcdef123456' 2023-11-22 12:35:01.234 INFO 12345 --- [extShutdownHook] o.testcontainers.DemoApplicationTests : Container with ID 'abcdef123456' has been stopped
4. Conclusion
In conclusion, the built-in Testcontainers support in Spring Boot represents a powerful toolset for simplifying and enhancing the process of integration testing within Spring applications. Testcontainers seamlessly integrate with the Spring testing framework, providing a convenient and efficient way to manage containerized dependencies, such as databases or message brokers, during testing. By leveraging Testcontainers annotations, developers can dynamically create and manage containers for their tests, ensuring a consistent and isolated environment.
The use of Testcontainers in Spring Boot applications brings several notable advantages. It promotes a more realistic testing environment by allowing tests to interact with actual containerized services, closely resembling production setups. This approach facilitates end-to-end testing, uncovering potential issues related to container-specific configurations and ensuring that the application behaves as expected in real-world scenarios.
Moreover, Testcontainers simplifies the setup and teardown process, automatically managing the lifecycle of containers, which leads to cleaner and more maintainable test code. The integration is extensible, supporting various container types and configurations, making it adaptable to different testing requirements.
However, it’s essential to carefully manage the lifecycle of containers to avoid resource leaks and potential conflicts between tests. Additionally, while Testcontainers excels in integration testing, its use should be complemented with other testing strategies, such as unit tests and mocking, to create a comprehensive and well-balanced testing suite.