
Introduction
In this blog, we will explore how to write efficient integration tests for your Spring Boot APIs using Testcontainers. Testing is a crucial part of development as it ensures that your code functions correctly and that changes do not break existing functionality. Proper testing also promotes good design practices in your codebase.
Different Layers in a Spring Boot API
Let's get started by understanding the typical architectural layers in a Spring Boot API.
- Controller: Entry point for your API.
- Service Layer: Performs your business logic.
- Repository: Handles database or data access.
From service or repository layers, you may interact with external services like databases, message queues, cloud storage etc.
Different Types of Tests
For applications, you can write different types of tests based on the scope and purpose of the test. Here are the three main types of tests:
-
Unit Tests:
- Purpose: Test individual units of code in isolation. Separate tests for Controller, Service, Repository.
- Tools: JUnit, Mockito etc.
- Characteristics: Fast, easy to write, and use mocking to isolate dependencies.
-
Integration Tests:
- Purpose: Test the interaction between different parts of the application (e.g., controller, service layer, repository, database, external services)
- Tools: Spring Boot Test Framework, Testcontainers, Rest Assured, Cucumber, Karate etc.
- Characteristics: Slower than unit tests, ensure components work together, can involve real external systems.
-
End-to-End (E2E) Tests:
- Purpose: Test the entire application flow like how it operates in production.
- Tools: Selenium, Cypress, Playwright etc.
- Characteristics: Slowest, provide the highest confidence, simulate real user interactions.
In this tutorial, we will create an integration test for an API that integrates with MongoDB and S3. This API is part of a social media app video tutorial. This API has multiple endpoints, we will be writing integration tests for user registration and login. The registration endpoint accepts details like name, email, password, and a profile photo, storing data in MongoDB and the profile photo in S3. Login endpoint authenticates the user and returns a JWT token.
@RestController @RequestMapping("/user") @AllArgsConstructor public class UserController { private final UserService userService; @PostMapping("/signup") public ResponseEntity<User> register( @RequestParam String name, @RequestParam String email, @RequestParam String password, @RequestParam(required = false) MultipartFile profilePhoto) throws IOException { UserDto registerUserDto = new UserDto(name, email, password, profilePhoto); User registeredUser = userService.register(registerUserDto); return ResponseEntity.ok(registeredUser); } @PostMapping("/signin") public ResponseEntity<LoginResponse> authenticate(@RequestBody LoginDto loginUserDto) { var loginResponse = userService.authenticate(loginUserDto); return ResponseEntity.ok(loginResponse); } }
Why Use Testcontainers for integration tests?
When conducting integration tests, it's crucial to test the end-to-end API, including integrations with databases, S3, and other services. One approach is to use a shared instance for testing, but this can cause conflicts in a shared environment, especially when tests run in parallel as part of a CI/CD pipeline. Another approach is to use in-memory databases like H2, but this doesn't accurately reflect the production environment, leading to potential discrepancies. This is where Testcontainers come in. Testcontainers provide an API to run production-like software in lightweight containers. This means you can run the same versions of databases, Redis, S3, etc., as part of your tests. Once the tests are complete, the containers are automatically cleaned up. This ensures that your code is tested against the exact versions of software used in production, providing more reliable and accurate test results.
Setting Up the Project
First, let's set up the project with the necessary dependencies:
- Spring Boot Testcontainers: Provides support for using Testcontainers in Spring Boot tests.
- MongoDB Testcontainers: Allows you to spin up a MongoDB container for integration testing.
- LocalStack Testcontainers: Enables the use of LocalStack for testing AWS services like S3.
- Rest Assured: A fluent library for testing and validating REST APIs.
Adding Dependencies
Add the following dependencies to your pom.xml
or build.gradle
file:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>mongodb</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>localstack</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency>
Creating the Integration Test
Let's create an integration test for our Spring Boot API. We will use Testcontainers to spin up MongoDB and LocalStack containers, and Rest Assured to test the API endpoints.
Basic Test Class Setup
First, we define a basic test class for our integration tests. We use the @SpringBootTest
annotation to load the full application context and set the web environment to a random port. The @Testcontainers
annotation is used to enable support for Testcontainers. Spring Boot Testcontainers provides lifecycle management for Testcontainers, ensuring that the containers are started before the tests run and stopped after the tests complete.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Testcontainers public class UserControllerIntegrationTest { @LocalServerPort private int port; }
@LocalServerPort
is used to inject the random port assigned to the application. This port will be used to call the end point using Rest Assured. While running tests it is important to use a random port to avoid conflicts with other running tests.
Define the MongoDB and LocalStack Containers
Next, we add Testcontainers for MongoDB and LocalStack. The @Container
annotation is used to define the containers.
@ServiceConnection
ensures that Spring uses test container connection for database operations.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Testcontainers public class UserControllerIntegrationTest { @LocalServerPort private int port; @Container @ServiceConnection final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10"); @Container final static LocalStackContainer localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:3.5.0")) .withServices(LocalStackContainer.Service.S3); }
Configure Dynamic Properties for AWS S3
We use the @DynamicPropertySource annotation to configure dynamic properties for AWS S3. This ensures that the properties used in the application for S3 connection here are set correctly for the LocalStack container.
@DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("aws.s3.accessKey", localStackContainer::getAccessKey); registry.add("aws.s3.secretKey", localStackContainer::getSecretKey); registry.add("aws.s3.region", localStackContainer::getRegion); registry.add("aws.s3.endpoint", () -> localStackContainer.getEndpointOverride(LocalStackContainer.Service.S3)); }
Setting Up Before All Tests
We use the @BeforeAll
annotation to set up the base URI for Rest Assured and create the S3 bucket before all tests.
@BeforeAll public static void setUp() { RestAssured.baseURI = "http://localhost"; createBucket(); } private static void createBucket() { S3Client s3Client = S3Client.builder() .endpointOverride(localStackContainer.getEndpointOverride(LocalStackContainer.Service.S3)) .credentialsProvider(StaticCredentialsProvider.create( AwsBasicCredentials.create(localStackContainer.getAccessKey(), localStackContainer.getSecretKey()))) .region(Region.of(localStackContainer.getRegion())) .serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(true).build()) .build(); s3Client.createBucket(CreateBucketRequest.builder().bucket(AWSConfig.BUCKET_NAME).build()); }
Writing the Test for User Registration
Now let us add a test method to verify the user registration functionality. This test sends a multipart form-data request to the /user/signup endpoint and verifies the response.
@Test @Order(1) public void testRegisterUser() { UserDto userDto = new UserDto("John Doe", "john.doe@example.com", "password123", null); File profilePhoto = new File("src/test/resources/profile-photo.jpg"); given() .port(port) .contentType("multipart/form-data") .multiPart("name", userDto.name()) .multiPart("email", userDto.email()) .multiPart("password", userDto.password()) .multiPart("profilePhoto", profilePhoto) .when() .post("/user/signup") .then() .statusCode(200) .body("name", equalTo(userDto.name())) .body("email", equalTo(userDto.email())); }
Writing the Test for User Login
Let us write another test method to verify the user authentication functionality. This test sends a JSON request to the /user/signin endpoint and verifies the response.
@Test @Order(2) public void testAuthenticateUser() { LoginDto loginDto = new LoginDto("john.doe@example.com", "password123"); given() .port(port) .contentType("application/json") .body(loginDto) .when() .post("/user/signin") .then() .statusCode(200) .body("token", notNullValue()); }
Order
annotation is used to specify the order of test execution. The first test registers a user, and then the second test authenticates the user. These tests use Rest Assured's fluent API to set up the request, send it, and validate the response.
Step 3: Running the Tests
Run the tests using your preferred method (e.g., IDE, Maven, Gradle). The tests will spin up the MongoDB and LocalStack containers, run the tests, and then shut down the containers.
Conclusion
In this tutorial, we learned how to create efficient integration tests for a Spring Boot API using Testcontainers and Rest Assured. By using Testcontainers, we can ensure that our tests run against real instances of MongoDB and AWS S3, providing more reliable and accurate test results.
To stay updated with the latest updates in Java and Spring follow us on youtube, linked in and medium.
You can find the code used in this blog here.
Video Version
To watch a more detailed video version of creating integration tests for a Spring Boot API using Testcontainers, see the video below:
Related Posts
Mastering New RestClient API in Spring
This guide explores the features and usage of the RestClient introduced in Spring 6, providing a modern and fluent API for making HTTP requests. It demonstrates how to create and customize RestClient instances, make API calls, and handle responses effectively.
Docker Basics for Developers
Learn the basics of Docker, a powerful tool for developing, shipping, and running applications. This guide covers setting up Docker, creating and managing containers, and best practices for developers.