1. Overview
In this lesson, we’ll learn how to use MongoDBContainer to test against a real MongoDB instance. In a previous lesson, we used PostgreSQLContainer to work with a relational database. Now we’ll apply the specialized container pattern to a NoSQL database and see how the approach differs.
We’ll start by configuring the container with a specific Docker image, then connect to it using the raw MongoDB Java driver and the dynamically provided connection string. Then we’ll pass the connection to a CampaignMongoRepository and set up campaign data programmatically. From there, we’ll write tests that exercise the repository against the container.
The relevant module we need to import when starting this lesson is: nosql-testing-start.
If we want to reference the fully implemented lesson, we can import: nosql-testing-end.
2. Setting Up MongoDBContainer
Let’s begin by adding the dependencies we need. The Testcontainers MongoDB module provides the MongoDBContainer class, and the MongoDB Java driver gives us the client API to interact with the database:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-mongodb</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>${mongodb-driver.version}</version>
</dependency>
Note that mongodb-driver.version is already defined in the start module POM, so we don’t need to add it manually.
With the dependencies in place, let’s create a test class and declare our container:
@Testcontainers
class MongoDBContainerIntegrationTest {
@Container
static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:7");
// ...
}
We’re passing mongo:7 as the Docker image, which pins us to a specific major version of MongoDB for stable and predictable behavior. Unlike PostgreSQLContainer, MongoDBContainer has no configuration methods for setting a username, password, or database name. There’s no withUsername(), withPassword(), or withDatabaseName() here. MongoDB starts without authentication by default in a test context, so there’s nothing extra to configure.
It’s worth mentioning that we’re using the @Testcontainers and @Container annotations for lifecycle management, as we’ve seen in a previous lesson. The container starts before any test method runs and stops after all tests complete.
One detail worth noting: MongoDBContainer starts MongoDB with the –replSet flag by default. This means the instance runs as a single-node replica set, which enables transaction support out of the box.
3. Connecting to MongoDB
With the container in place, we need to connect to it.
3.1. Obtaining a MongoClient
Since Testcontainers assigns a random host port at startup, we can’t hardcode the connection details. Instead, MongoDBContainer provides a dynamically generated connection string through its getConnectionString() method:
MongoClient client = MongoClients.create(mongoDBContainer.getConnectionString());
The getConnectionString() method returns a fully constructed mongodb:// URI that includes the correct host and the dynamically mapped port. This is important because the port changes every time the container starts, so hardcoding a connection string would be unreliable. We pass the URI directly to MongoClients.create() to obtain a MongoClient instance.
3.2. Initializing the Repository
We’ll pass the client to CampaignMongoRepository, which uses it internally to access a database and collection:
public class CampaignMongoRepository {
private final MongoCollection<Document> collection;
public CampaignMongoRepository(MongoClient client) {
MongoDatabase database = client.getDatabase("ltc");
this.collection = database.getCollection("campaigns");
}
public long count() {
// ...
}
public Document findByCode(String code) {
// ...
}
}
This keeps the connection management outside the repository. The repository receives a MongoClient from the caller — in a real Spring application, this would be a pre-configured bean injected from the application context. In the test, we create and wire it directly using the container’s connection string.
Now, let’s have a look at the complete client setup method:
@BeforeAll
static void setUpClient() {
client = MongoClients.create(mongoDBContainer.getConnectionString());
repository = new CampaignMongoRepository(client);
}
Notice that we create the MongoClient once in @BeforeAll and share it across all tests, rather than recreating it in each @BeforeEach. The client holds a network connection to the container, so creating it once avoids unnecessary overhead.
Since the client is opened in @BeforeAll, it should be closed in @AfterAll. This is the natural complement — open once, close once:
@AfterAll
static void tearDown() {
client.close();
}
4. Setting Up Test Data and Isolation
Before we can test the repository, we need to seed the database with campaign documents. SQL-oriented Testcontainers modules support loading initial data from a SQL script. MongoDBContainer doesn’t support that, so we set up data programmatically using the MongoClient API directly in a @BeforeEach method.
4.1. Inserting Documents
MongoDB doesn’t require us to define a schema before inserting data. SQL databases require a schema definition before data can be inserted, but here we can insert documents with any structure directly. The database and collection are created automatically the first time we write data to them.
Let’s insert three campaign documents into a campaigns collection:
@BeforeEach
void setUp() {
MongoDatabase database = client.getDatabase("ltc");
collection = database.getCollection("campaigns");
collection.insertMany(
List.of(
new Document("code", "C1").append("name", "Campaign 1")
.append("description", "Description of Campaign 1"),
new Document("code", "C2").append("name", "Campaign 2")
.append("description", "About Campaign 2"),
new Document("code", "C3").append("name", "Campaign 3")
.append("description", "About Campaign 3")
)
);
}
Each Document is a key-value structure similar to a JSON object. We build it using a fluent API where append() adds additional fields. The insertMany() method writes all three documents to the collection in a single call.
4.2. Ensuring Test Isolation
When multiple test methods insert data into the same collection, tests can interfere with each other. A test might see leftover documents from a previous test, causing unexpected assertion failures. The simplest way to prevent this is to drop the collection before seeding it:
collection = database.getCollection("campaigns");
collection.drop();
collection.insertMany(List.of(...));
Calling collection.drop() removes the collection and all its documents, giving each test a clean slate. MongoDB recreates the collection automatically the next time we insert data, so there’s no need for an explicit create step after the drop.
There are other approaches we could take here. We could use a separate database name for each test, or restart the container between test methods. However, using separate databases adds unnecessary complexity, and restarting the container is significantly slower since it involves tearing down and recreating the Docker container. Dropping the collection in @BeforeEach is the preferred option because it’s simple, fast, and sufficient for the vast majority of test scenarios.
5. Writing the Tests
The container is running, the MongoClient connects to it, the repository is initialized, and the @BeforeEach method seeds fresh data before each test. Let’s write the test methods that exercise CampaignMongoRepository.
Our first test verifies that the repository’s count() method returns the correct number of campaigns:
@Test
void givenCampaignDocuments_whenCounting_thenReturnsExpectedCount() {
long count = repository.count();
assertEquals(3, count);
}
From our seed data, we inserted three campaign documents, so count() should return 3.
Now let’s add a second test that queries a specific campaign by code:
@Test
void givenCampaignDocuments_whenFindingByCode_thenReturnsCorrectDocument() {
Document result = repository.findByCode("C1");
assertEquals("Campaign 1", result.getString("name"));
assertEquals("Description of Campaign 1", result.getString("description"));
}
The findByCode() method looks up a campaign document where the code field matches “C1” and returns it. We then verify that the returned document has the expected name and description values, matching the seed data.
Because the @BeforeEach method drops and re-seeds the collection before every test, these two tests are completely independent. They can run in any order without interfering with each other.
Let’s verify by running mvn test at this point. Both tests should pass, confirming that the container starts correctly, the connection string works, and our isolation strategy keeps each test independent.
6. Conclusion
In this lesson, we configured a MongoDBContainer with the mongo:7 image, connected to it using the raw MongoClient driver and the dynamically provided connection string, injected the client into CampaignMongoRepository, seeded campaign data programmatically, and ensured test isolation by dropping collections between tests.
Compared to PostgreSQLContainer, the core pattern remains the same: Testcontainers manages the container lifecycle and provides the connection details at runtime. The differences lie in the connection string format, the client API, and the data setup approach, which is programmatic rather than script-based.