MongoDB is one of the NoSQL databases that Spring Boot offers great testing support for. Like all other test slice annotations from Spring Boot, when using @DataMongoTest, we'll get a Spring Test Context with just enough beans to test any MongoDB-related code. By default, Spring Boot tries to start and use an embedded MongoDB instance. We'll disable the default behavior of @DataMongoTest with this blog post to plug in a MongoDB Docker container instead and use Testcontainers for this purpose.
Spring Boot Project Setup for MongoDB and Testcontainers
For demonstration purposes, let's use a basic Spring Boot application. As we are about to test MongoDB-related code, we going to use the Spring Boot Starter for MongoDB. Apart from this starter, the project includes the Spring Boot Starter Web and Test (aka. the swiss-army knife for testing).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>de.rieckpil.blog</groupId> <artifactId>spring-data-mongo-test-testcontainers</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-data-mongo-test-testcontainers</name> <properties> <java.version>11</java.version> <testcontainers.version>1.15.0</testcontainers.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>mongodb</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers-bom</artifactId> <version>${testcontainers.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> |
The only non-Spring Boot dependencies for this project are two test dependencies from Testcontainers. The junit-jupiter
dependency contains the Testcontainers JUnit Jupiter extension, which we're going to use to manage our container's lifecycle. mongodb
includes the MongoDB Testcontainers Module. To manage and align the version of both imports, we add the testcontainers-bom
(Bill of Materials) to the dependencyManagement
block.
Introduction To @DataMongoTest
Similar to @DataJpaTest
, when writing tests that involve a relational database, with @DataMongoTest
, we get a sliced Spring Context. This Spring Test Context contains all relevant MongoDB components of our Spring Boot application.
This includes:
- a configured
MongoTemplate
(a utility for several basic MongoDB operations, similar to theJdbcTemplate
) - a scan for all
@Document
objects (our collection entities) - all Spring Data MongoDB repositories (all our interfaces that extends the
MongoRepository
interface)
All other components (e.g. @Controller
, @Service
, @Component
) are not part of this context.
When using this annotation, Spring Boot tries to start and auto-configure an embedded MongoDB database for us. The relevant logic is part of the EmbeddedMongoAutoConfiguration
class.
This embedded database setup works for a lot of use cases. However, when we want to tweak the MongoDB server configuration, this mechanism might not be the best.
Furthermore, to use a pre-filled database with a production-like payload for testing, we might be better off with an already prepared Docker image. That's where Testcontainers comes into play. With Testcontainers we can manage the lifecycle of a Docker container for our tests.
Testcontainers already provides a MongoDB module (currently in incubating mode), making the setup even less complicated, especially when working with a MongoDB server version > 4.
Let's see how we can integrate Testcontainers with @DataMongoTest
to use a dockerized MongoDB instance.
Accessing MongoDB with a Spring Data MongoRepository
As a first example, let's write a test for a custom repository for one of our documents. The relevant @Document
class stores basic information about a customer:
1 2 3 4 5 6 7 8 9 10 11 12 | @Document public class Customer { @Id private String id; private String email; private Integer rating; // constructors, getters & setters } |
By adding a CustomerRepository
interface that extends the MongoRepository
inteface we get access to all well-known repository methods like .save()
, .delete()
, .findAll()
.
We can enrich this repository with custom queries that might use the derived query feature of Spring Data:
1 2 3 4 5 | public interface CustomerRepository extends MongoRepository<Customer, String> { @Query(sort = "{ rating : 1 }") List<Customer> findByRatingBetween(int from, int to); } |
Whether or not this query method is worth testing in isolation is not relevant for this blog post, as we are focussing on the required setup steps for using @DataMongoTest
with Testcontainers. In general, try not to test the framework (e.g. simple derived queries) and instead focus on testing more advanced custom queries (e.g. JSON-based Queries with SpEL Expressions).
Test Setup For @DataMongoTest And Testcontainers
With this in mind, we can move on to writing tests for the CustomerRepository
. We can break down the required setup into the following steps:
- Disable the auto-configuration for an embedded MongoDB
- Define our MongoDB Docker container and start it before executing the test
- Override the
spring.data.mongodb.uri
to point to the local database container (Testcontainers exposes a random ephemeral port)
We can accomplish the first step by excluding the EmbeddedMongoAutoConfiguration
for our test. There are multiple ways to exclude a Spring Boot auto-configuration for a test. We're going to use the excludeAutoConfiguration
attribute of the @DataMongoTest
annotation.
Next comes the Docker container definition. With the help of the MongoDB module for Testcontainers, we can instantiate a MongoDBContainer
and pass the Docker Image name of our choice. Make sure to specify the same MongoDB version that is used in production. We are not limited to use the official mongo
Docker image here and can also provide a custom image that e.g. builds on top of the official image and tweaks the config or pre-populates data.
The Testcontainers JUnit Jupiter extension that we register with @Testcontainers
takes care of the container lifecycle (starting/stopping).
What's left is to override the relevant Spring Boot property to connect to our local dockerized MongoDB instance. With @DynamicPropertySource
we can solve this in an elegant way prior to starting the Spring Test Context.
Putting it all together, the full test for verifying our custom query looks like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | @Testcontainers @DataMongoTest(excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class) class CustomerRepositoryTest { @Container static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.4.2"); @DynamicPropertySource static void setProperties(DynamicPropertyRegistry registry) { registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl); } @Autowired private CustomerRepository customerRepository; @AfterEach void cleanUp() { this.customerRepository.deleteAll(); } @Test void shouldReturnListOfCustomerWithMatchingRate() { List<Customer> customers = customerRepository.findByRatingBetween(40, 56); assertEquals(2, customers.size()); } |
For projects that are using Spring Boot < 2.2.6 @DynamicPropertySource
is not available yet. Refer to this article for a solution to configure the relevant Spring Boot property instead.
Verifying Code That Makes Use Of The MongoTemplate with @DataMongoTest
As an additional example, let's see how we can use this setup to test a service class that uses the MongoTemplate
.
Our CustomerService
class has one public method to find all customers that we consider as VIP:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Service public class CustomerService { private final MongoTemplate mongoTemplate; public CustomerService(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } public List<Customer> findAllVIPCustomers() { return mongoTemplate.find(Query.query(Criteria.where("rating").gte(90)), Customer.class); } } |
We can use the exact same setup for testing the CustomerService
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | @Testcontainers @DataMongoTest(excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class) class CustomerServiceTest { @Container static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.4.2"); @DynamicPropertySource static void setProperties(DynamicPropertyRegistry registry) { registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl); } @Autowired private MongoTemplate mongoTemplate; private CustomerService cut; @BeforeEach void setUp() { this.cut = new CustomerService(mongoTemplate); } @Test void shouldReturnCustomersWithRatingGreater90AsVIP() { List<Customer> result = cut.findAllVIPCustomers(); assertEquals(2, result.size()); } } |
The main difference here is that we have to instantiate the class under test on our own as it is not part of the Spring Test Context. In the previous example, the repository was already part of it, and hence we were able to inject it.
For this example, we could also write a plain unit test (if there would be more business logic) and use Mockito to mock the behavior of the MongoTemplate
.
We can further tweak the Testcontainers setup to either reuse already started Docker containers or start one container for all our tests.
You can find additional Testcontainer recipes in the following articles:
- Write Spring Boot Integration Tests With Testcontainers (JUnit 4 & 5)
- Reuse Containers With Testcontainers for Fast Integration Tests
- Testing Spring Boot Applications with Kotlin and Testcontainers
The source code for this example is available on GitHub.
Have fun writing MongoDB tests with @DataMongoTest and Testcontainers,
Philip