Streamlining Java Application Development With MongoDB: A Comprehensive Guide to Using Testcontainers
Aasawari Sahasrabuddhe6 min read • Published Jul 22, 2024 • Updated Jul 22, 2024
FULL APPLICATION
Rate this article
The world of software development is a continuous cycle. We build, test, deploy, and iterate – striving to deliver high-quality applications. Within this software development lifecycle (SDLC), testing is crucial in ensuring our code functions as intended. There are two main types of tests we encounter:
- Unit testing: In this part, the intent is to check if the business logic for the code is working well and if no unexpected results are formed.
- Integration testing: In this part of testing, the developer assesses how different parts of the system interact with each other. They simulate real-world scenarios by integrating with external dependencies. While providing a more holistic view of application behavior, traditional integration tests can be cumbersome to set up and maintain due to the need for external resources like databases.
Testcontainers is an open-source framework for Java that leverages the power of Docker containers to streamline integration testing, by spinning up lightweight, throwaway instances of real dependencies within Docker containers for the duration of your test.
This tutorial will explore using Testcontainers with Java and Spring Boot, leveraging MongoDB as our database. We'll show you how to set up Testcontainers, integrate it into your project, and effectively use it to streamline your integration tests.
Let's get started.
- Java version 22 — download and install the latest JDK from the official Oracle website
- Maven version 3.9.6 — Download and install the latest version from the Maven official website
- Docker version 26.0.0 — Download and install Docker Desktop for your operating system from the official website
As a software developer, you navigate through every phase of the software development lifecycle during application development. Once the development phase is complete, the next critical stage involves writing test cases to verify the business logic of the application.
Whether your application is developed using vanilla Java or Spring Boot, you will typically write JUnit tests to ensure that the business logic functions correctly and meets the specified requirements. These tests are essential for validating the application's functionality and maintaining high-quality standards.
To test the complete functionality of your application, you are also required to perform integration testing. Integration testing is particularly important as it focuses on the interactions between different modules of the software. It detects interface issues and validates the functional relationships and data flow between combined units, ensuring they work together seamlessly. This is crucial for systems with multiple interconnected components, as it improves the overall reliability and stability of the application by ensuring that all parts integrate correctly and function as a unified whole.
Now, let's suppose your Java application uses MongoDB as its database. While writing your JUnits tests, what would you prefer?
- Testing your services with in-memory databases that may also be utilized by another module of the application?
- Testing the application with the database created locally only for the business logic to be tested?
As a part of the database optimization, you would choose the second option from the above.
Now, this is where Testcontainers would help achieve the second option.
Let us understand the concept of Testcontainers in the next section.
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.
These containers are managed during test execution, ensuring a clean, isolated environment for each test or test suite. By leveraging Docker, Testcontainers allows developers to automate the setup and teardown of test dependencies, significantly simplifying the integration testing process.
As we have already discussed above, using Testcontainers gives you the advantage of using the database locally, reducing the load on the actual database used by the services.
Along with this, Testcontainers also helps in providing:
- Consistency: It ensures that the tests run in a consistent environment, regardless of the underlying infrastructure.
- Isolation: Each test can run in an isolated environment with its own set of dependencies.
- Up-to-date environments: This allows you to easily switch to different versions of dependencies by using different Docker images, which is particularly useful for testing compatibility with various versions of a database or service.
By now, you should have the theoretical knowledge about what Testcontainers is and why we should use it.
In this section of the article, we will discuss how Testcontainers can be used in Java and Spring Boot.
This will be divided into two sections:
- Testcontainers with Java
- Testcontainers with Spring Boot
The concept of Testcontainers remains the same irrespective of the framework being used, but these two sections will help you understand the application in both scenarios.
To begin using Testcontainers, you need to load the below dependency in your pom.xml file.
1 <dependency> 2 <groupId>org.mongodb</groupId> 3 <artifactId>mongodb-driver-sync</artifactId> 4 <version>5.1.0</version> 5 </dependency> 6 <dependency> 7 <groupId>org.junit.jupiter</groupId> 8 <artifactId>junit-jupiter-api</artifactId> 9 <version>5.10.2</version> 10 <scope>test</scope> 11 </dependency> 12 <dependency> 13 <groupId>org.junit.jupiter</groupId> 14 <artifactId>junit-jupiter-engine</artifactId> 15 <version>5.10.2</version> 16 <scope>test</scope> 17 </dependency> 18 <dependency> 19 <groupId>org.mockito</groupId> 20 <artifactId>mockito-core</artifactId> 21 <version>5.12.0</version> 22 <scope>test</scope> 23 </dependency> 24 <dependency> 25 <groupId>org.testcontainers</groupId> 26 <artifactId>testcontainers</artifactId> 27 <version>1.19.8</version> 28 <scope>test</scope> 29 </dependency> 30 <dependency> 31 <groupId>org.testcontainers</groupId> 32 <artifactId>junit-jupiter</artifactId> 33 <version>1.19.8</version> 34 <scope>test</scope> 35 </dependency> 36 <dependency> 37 <groupId>org.testcontainers</groupId> 38 <artifactId>mongodb</artifactId> 39 <version>1.19.8</version> 40 <scope>test</scope> 41 </dependency>
The test cases written in JUnits tests are executed in three stages with Testcontainers.
- Before tests: In this stage, the container is created, the database will be loaded, and collections will be created.
- During tests: This is the stage where the actual business logic of the application is tested.
- After tests: At this stage, the containers created in the first stage are destroyed and the resources are set free.
In this code example, the JUnits test case is written to test a simple query to find five movie names that have IMDb ratings greater than seven.
The JUnits tests are written in the TestcontainerClass.java class and the below description will help you understand the code:
If you look at the code, it is divided in four parts:
- Creating container: This will pull the mongo:7.0.0 docker image from Docker Hub and create the container. The @Container annotation will start and stop the container when the tests are finished.
1 @Container 2 private static final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:7.0.0");
- Setup: The container is started and the example documents are inserted inside
testCollection
.
1 @BeforeAll 2 public void setup() { 3 String uri = mongoDBContainer.getConnectionString(); 4 mongoClient = MongoClients.create(uri); 5 database = mongoClient.getDatabase("testdb"); 6 collection = database.getCollection("testCollection"); 7 8 // Insert sample data 9 Document doc1 = new Document("title", "Inception").append("imdb", new Document("rating", 8.8)); 10 Document doc2 = new Document("title", "The Room").append("imdb", new Document("rating", 3.7)); 11 Document doc3 = new Document("title", "The Dark Knight").append("imdb", new Document("rating", 9.0)); 12 13 collection.insertMany(List.of(doc1, doc2, doc3)); 14 }
- Actual tests: The actual test case for the business logic is implemented.
1 @Test 2 void testMoviesWithHighRating() { 3 List<Document> resultDocuments = new ArrayList<>(); 4 try (MongoCursor<Document> cursor = collection.find(Filters.gt("imdb.rating", 7)) 5 .projection(new Document("title", 1).append("_id", 0)) 6 .limit(5) 7 .iterator()) { 8 while (cursor.hasNext()) { 9 Document doc = cursor.next(); 10 System.out.println(doc.toJson()); 11 resultDocuments.add(doc); 12 } 13 } 14 15 assertEquals(2, resultDocuments.size()); 16 for (Document doc : resultDocuments) { 17 assertTrue(doc.containsKey("title")); 18 assertFalse(doc.containsKey("_id")); 19 } 20 }
In the above code example, when the sample data is loaded, the resultDocuments will contain an empty list.
Next, in the try-catch section of the code, the cursor iterates through the results, adding each document to the resultDocuments list after printing it in JSON format to the console. After the iteration, the test asserts that exactly two documents were retrieved by checking the size of the resultDocuments list.
Additionally, for each document in the list, the test asserts that the title field is present and the _id field is absent. These assertions ensure that the query behaves as expected, retrieving the correct number of documents with the appropriate fields.
It is often confusing that the tests are created on the production database. But the advantage of using Testcontainers is that when you run the test class, the Docker image is loaded, the sample document is created, and the test cases are run all on this container. After the tests are completed, the container and connection are closed.
Now, let us try to understand how this logic can be implemented in the Spring Boot framework.
To write JUnits tests in a Spring Boot application, we will utilize an old GitHub repository that explains advanced aggregations using Spring Boot.
In this repository, you will see different REST APIs created that work on the sample_supplies.sales collection.
For this example, we will just utilize it to write test cases for the findAll() API.
The JUnits tests are similarly written in MongoDBSalesRepositoryTest.java.
- Set up the container:
1 @Container 2 public static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:7.0.0"));
- Insert data into the container:
1 @DynamicPropertySource 2 static void setProperties(DynamicPropertyRegistry registry) { 3 registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl); 4 registry.add("spring.data.mongodb.database", () -> "testdb"); 5 } 6 7 @BeforeAll 8 public static void setUpAll() { 9 String mongoUri = mongoDBContainer.getConnectionString(); 10 mongoClient = MongoClients.create(mongoUri); 11 } 12 13 @BeforeEach 14 public void setUp() { 15 salesRepository.save(createMockSales()); 16 } 17 18 private Sales createMockSales() { 19 Item item1 = new Item("Item1", Arrays.asList("Tag1", "Tag2"), new BigDecimal("19.99"), 2); 20 Item item2 = new Item("Item2", Arrays.asList("Tag3", "Tag4"), new BigDecimal("29.99"), 1); 21 List<Item> items = Arrays.asList(item1, item2); 22 23 Customer customer = new Customer("Male", 30, "customer@example.com", 5); 24 25 return new Sales(new ObjectId(), new Date(), items, "Store1", customer, true, "Online"); 26 }
- Actual tests:
1 @Test 2 public void testFindAll() { 3 List<Sales> salesList = salesRepository.findAll(); 4 assertThat(salesList).isNotEmpty(); 5 assertThat(salesList.size()).isEqualTo(1); 6 Sales sales = salesList.get(0); 7 assertThat(sales.getStoreLocation()).isEqualTo("Store1"); 8 assertThat(sales.getCustomer().getEmail()).isEqualTo("customer@example.com"); 9 assertThat(sales.getItems()).hasSize(2); 10 }
The testFindAll method validates the findAll function of SalesRepository. It checks if at least one sales record is returned if there's exactly one record, and if its attributes match expected values like store location, customer email, and the number of items purchased. This test ensures the correctness of the repository's retrieval function.
Throughout this tutorial, we've demonstrated how to set up and use Testcontainers for integration testing with MongoDB in both a vanilla Java application and a Spring Boot application. We started by adding the necessary dependencies to our project, setting up the MongoDB container, and writing test cases to validate our business logic. Following these steps can achieve a more robust and maintainable testing setup.
If you liked this article and wish to practice more such content, we encourage you to visit the MongoDB Developer Centre. You can also visit our community forums for meaningful discussions and documentation for more learning.
Top Comments in Forums
There are no comments on this article yet.