Skip to content

Commit

Permalink
Add MongoDB Atlas implementation (#9290)
Browse files Browse the repository at this point in the history
Co-authored-by: Luke Thompson <endeavour9@gmail.com>
  • Loading branch information
eddumelendez and luketn authored Sep 30, 2024
1 parent 04206d9 commit 8921a5a
Show file tree
Hide file tree
Showing 5 changed files with 352 additions and 2 deletions.
42 changes: 40 additions & 2 deletions docs/modules/databases/mongodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
!!! note
This module is INCUBATING. While it is ready for use and operational in the current version of Testcontainers, it is possible that it may receive breaking changes in the future. See [our contributing guidelines](/contributing/#incubating-modules) for more information on our incubating modules policy.

## Usage example
The MongoDB module provides two Testcontainers for MongoDB unit testing:

* [MongoDBContainer](#mongodbcontainer) - the core MongoDB database
* [MongoDBAtlasLocalContainer](#mongodbatlaslocalcontainer) - the core MongoDB database combined with MongoDB Atlas Search + Atlas Vector Search

## MongoDBContainer

### Usage example

The following example shows how to create a MongoDBContainer:

Expand Down Expand Up @@ -36,6 +43,37 @@ For instance, to initialize a single node replica set on fixed ports via Docker,
As we can see, there is a lot of operations to execute and we even haven't touched a non-fixed port approach.
That's where the MongoDBContainer might come in handy.

## MongoDBAtlasLocalContainer

### Usage example

The following example shows how to create a MongoDBAtlasLocalContainer:

<!--codeinclude-->
[Creating a MongoDB Atlas Local Container](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainerTest.java) inside_block:creatingAtlasLocalContainer
<!--/codeinclude-->

And how to start it:

<!--codeinclude-->
[Start the Container](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainerTest.java) inside_block:startingAtlasLocalContainer
<!--/codeinclude-->

The connection string provided by the MongoDBAtlasLocalContainer's getConnectionString() method includes the dynamically allocated port:

<!--codeinclude-->
[Get the Connection String](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainerTest.java) inside_block:getConnectionStringAtlasLocalContainer
<!--/codeinclude-->

e.g. `mongodb://localhost:12345/?directConnection=true`

### References
MongoDB Atlas Local combines the MongoDB database engine with MongoT, a sidecar process for advanced searching capabilities built by MongoDB and powered by [Apache Lucene](https://lucene.apache.org/).

The container (mongodb/mongodb-atlas-local) documentation can be found [here](https://www.mongodb.com/docs/atlas/cli/current/atlas-cli-deploy-docker/).

General information about Atlas Search can be found [here](https://www.mongodb.com/docs/atlas/atlas-search/).

## Adding this module to your project dependencies

Add the following dependency to your `pom.xml`/`build.gradle` file:
Expand All @@ -55,7 +93,7 @@ Add the following dependency to your `pom.xml`/`build.gradle` file:
```

!!! hint
Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency
Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency

#### Copyright
Copyright (c) 2019 Konstantin Silaev <silaev256@gmail.com>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.testcontainers.mongodb;

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;

/**
* Testcontainers implementation for MongoDB Atlas.
* <p>
* Supported images: {@code mongodb/mongodb-atlas-local}
* <p>
* Exposed ports: 27017
*/
public class MongoDBAtlasLocalContainer extends GenericContainer<MongoDBAtlasLocalContainer> {

private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mongodb/mongodb-atlas-local");

private static final int MONGODB_INTERNAL_PORT = 27017;

public MongoDBAtlasLocalContainer(final String dockerImageName) {
this(DockerImageName.parse(dockerImageName));
}

public MongoDBAtlasLocalContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);

withExposedPorts(MONGODB_INTERNAL_PORT);
waitingFor(Wait.forSuccessfulCommand("runner healthcheck"));
}

/**
* Get the connection string to MongoDB.
*/
public String getConnectionString() {
return String.format("mongodb://%s:%d/?directConnection=true", getHost(), getMappedPort(MONGODB_INTERNAL_PORT));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package org.testcontainers.mongodb;

import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.ListSearchIndexesIterable;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Aggregates;
import com.mongodb.client.model.search.SearchOperator;
import com.mongodb.client.model.search.SearchOptions;
import com.mongodb.client.model.search.SearchPath;
import org.bson.BsonDocument;
import org.bson.Document;
import org.bson.codecs.configuration.CodecRegistries;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;
import org.bson.conversions.Bson;
import org.bson.json.JsonWriterSettings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

import static org.awaitility.Awaitility.await;

public class AtlasLocalDataAccess implements AutoCloseable {

private static final Logger log = LoggerFactory.getLogger(AtlasLocalDataAccess.class);

private final MongoClient mongoClient;

private final MongoDatabase testDB;

private final MongoCollection<TestData> testCollection;

private final String collectionName;

public AtlasLocalDataAccess(String connectionString, String databaseName, String collectionName) {
this.collectionName = collectionName;
log.info("DataAccess connecting to {}", connectionString);

CodecRegistry pojoCodecRegistry = CodecRegistries.fromProviders(
PojoCodecProvider.builder().automatic(true).build()
);
CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(),
pojoCodecRegistry
);
MongoClientSettings clientSettings = MongoClientSettings
.builder()
.applyConnectionString(new ConnectionString(connectionString))
.codecRegistry(codecRegistry)
.build();
mongoClient = MongoClients.create(clientSettings);
testDB = mongoClient.getDatabase(databaseName);
testCollection = testDB.getCollection(collectionName, TestData.class);
}

@Override
public void close() {
mongoClient.close();
}

public void initAtlasSearchIndex() throws URISyntaxException, IOException, InterruptedException {
//Create the collection (if it doesn't exist). Required because unlike other database operations, createSearchIndex will fail if the collection doesn't exist yet
testDB.createCollection(collectionName);

//Read the atlas search index JSON from a resource file
String atlasSearchIndexJson = new String(
Files.readAllBytes(Paths.get(getClass().getResource("/atlas-local-index.json").toURI())),
StandardCharsets.UTF_8
);
log.info(
"Creating Atlas Search index AtlasSearchIndex on collection {}:\n{}",
collectionName,
atlasSearchIndexJson
);
testCollection.createSearchIndex("AtlasSearchIndex", BsonDocument.parse(atlasSearchIndexJson));

//wait for the atlas search index to be ready
Instant start = Instant.now();
await()
.atMost(5, TimeUnit.SECONDS)
.pollInterval(10, TimeUnit.MILLISECONDS)
.pollInSameThread()
.until(this::getIndexStatus, "READY"::equalsIgnoreCase);

log.info(
"Atlas Search index AtlasSearchIndex on collection {} is ready (took {} milliseconds) to create.",
collectionName,
start.until(Instant.now(), ChronoUnit.MILLIS)
);
}

private String getIndexStatus() {
ListSearchIndexesIterable<Document> searchIndexes = testCollection.listSearchIndexes();
for (Document searchIndex : searchIndexes) {
if (searchIndex.get("name").equals("AtlasSearchIndex")) {
return searchIndex.getString("status");
}
}
return null;
}

public void insertData(TestData data) {
log.info("Inserting document {}", data);
testCollection.insertOne(data);
}

public TestData findAtlasSearch(String test) {
Bson searchClause = Aggregates.search(
SearchOperator.of(SearchOperator.text(SearchPath.fieldPath("test"), test).fuzzy()),
SearchOptions.searchOptions().index("AtlasSearchIndex")
);
log.trace(
"Searching for document using Atlas Search:\n{}",
searchClause.toBsonDocument().toJson(JsonWriterSettings.builder().indent(true).build())
);
return testCollection.aggregate(Collections.singletonList(searchClause)).first();
}

public static class TestData {

String test;

int test2;

boolean test3;

public TestData() {}

public TestData(String test, int test2, boolean test3) {
this.test = test;
this.test2 = test2;
this.test3 = test3;
}

public String getTest() {
return test;
}

public void setTest(String test) {
this.test = test;
}

public int getTest2() {
return test2;
}

public void setTest2(int test2) {
this.test2 = test2;
}

public boolean isTest3() {
return test3;
}

public void setTest3(boolean test3) {
this.test3 = test3;
}

@Override
public String toString() {
return "TestData{" + "test='" + test + '\'' + ", test2=" + test2 + ", test3=" + test3 + '}';
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.testcontainers.mongodb;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

public class MongoDBAtlasLocalContainerTest {

private static final Logger log = LoggerFactory.getLogger(MongoDBAtlasLocalContainerTest.class);

@Test
public void getConnectionString() {
try (
MongoDBAtlasLocalContainer container = new MongoDBAtlasLocalContainer("mongodb/mongodb-atlas-local:7.0.9")
) {
container.start();
String connectionString = container.getConnectionString();
assertThat(connectionString).isNotNull();
assertThat(connectionString).startsWith("mongodb://");
assertThat(connectionString)
.isEqualTo(
String.format(
"mongodb://%s:%d/?directConnection=true",
container.getHost(),
container.getFirstMappedPort()
)
);
}
}

@Test
public void createAtlasIndexAndSearchIt() throws Exception {
try (
// creatingAtlasLocalContainer {
MongoDBAtlasLocalContainer atlasLocalContainer = new MongoDBAtlasLocalContainer(
"mongodb/mongodb-atlas-local:7.0.9"
);
// }
) {
// startingAtlasLocalContainer {
atlasLocalContainer.start();
// }

// getConnectionStringAtlasLocalContainer {
String connectionString = atlasLocalContainer.getConnectionString();
// }

try (
AtlasLocalDataAccess atlasLocalDataAccess = new AtlasLocalDataAccess(connectionString, "test", "test")
) {
atlasLocalDataAccess.initAtlasSearchIndex();

atlasLocalDataAccess.insertData(new AtlasLocalDataAccess.TestData("tests", 123, true));

Instant start = Instant.now();
log.info(
"Waiting for Atlas Search to index the data by polling atlas search query (Atlas Search is eventually consistent)"
);
await()
.atMost(5, TimeUnit.SECONDS)
.pollInterval(10, TimeUnit.MILLISECONDS)
.pollInSameThread()
.until(() -> atlasLocalDataAccess.findAtlasSearch("test"), Objects::nonNull);
log.info(
"Atlas Search indexed the new data and was searchable after {}ms.",
start.until(Instant.now(), ChronoUnit.MILLIS)
);
}
}
}
}
18 changes: 18 additions & 0 deletions modules/mongodb/src/test/resources/atlas-local-index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"mappings": {
"dynamic": false,
"fields": {
"test": {
"type": "string"
},
"test2": {
"type": "number",
"representation": "int64",
"indexDoubles": false
},
"test3": {
"type": "boolean"
}
}
}
}

0 comments on commit 8921a5a

Please sign in to comment.