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.
Let's get started by understanding the typical architectural layers in a Spring Boot API.
From service or repository layers, you may interact with external services like databases, message queues, cloud storage etc.
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:
Integration Tests:
End-to-End (E2E) Tests:
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); } }
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.
First, let's set up the project with the necessary 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>
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.
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.
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); }
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)); }
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()); }
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())); }
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.
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.
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.
To watch a more detailed video version of creating integration tests for a Spring Boot API using Testcontainers, see the video below:
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.
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.
Get instant AI-powered summaries of YouTube videos and websites. Save time while enhancing your learning experience.