Skip to content

Commit 8921a5a

Browse files
eddumelendezluketn
andauthored
Add MongoDB Atlas implementation (#9290)
Co-authored-by: Luke Thompson <endeavour9@gmail.com>
1 parent 04206d9 commit 8921a5a

File tree

5 files changed

+352
-2
lines changed

5 files changed

+352
-2
lines changed

docs/modules/databases/mongodb.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
!!! note
44
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.
55

6-
## Usage example
6+
The MongoDB module provides two Testcontainers for MongoDB unit testing:
7+
8+
* [MongoDBContainer](#mongodbcontainer) - the core MongoDB database
9+
* [MongoDBAtlasLocalContainer](#mongodbatlaslocalcontainer) - the core MongoDB database combined with MongoDB Atlas Search + Atlas Vector Search
10+
11+
## MongoDBContainer
12+
13+
### Usage example
714

815
The following example shows how to create a MongoDBContainer:
916

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

46+
## MongoDBAtlasLocalContainer
47+
48+
### Usage example
49+
50+
The following example shows how to create a MongoDBAtlasLocalContainer:
51+
52+
<!--codeinclude-->
53+
[Creating a MongoDB Atlas Local Container](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainerTest.java) inside_block:creatingAtlasLocalContainer
54+
<!--/codeinclude-->
55+
56+
And how to start it:
57+
58+
<!--codeinclude-->
59+
[Start the Container](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainerTest.java) inside_block:startingAtlasLocalContainer
60+
<!--/codeinclude-->
61+
62+
The connection string provided by the MongoDBAtlasLocalContainer's getConnectionString() method includes the dynamically allocated port:
63+
64+
<!--codeinclude-->
65+
[Get the Connection String](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainerTest.java) inside_block:getConnectionStringAtlasLocalContainer
66+
<!--/codeinclude-->
67+
68+
e.g. `mongodb://localhost:12345/?directConnection=true`
69+
70+
### References
71+
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/).
72+
73+
The container (mongodb/mongodb-atlas-local) documentation can be found [here](https://www.mongodb.com/docs/atlas/cli/current/atlas-cli-deploy-docker/).
74+
75+
General information about Atlas Search can be found [here](https://www.mongodb.com/docs/atlas/atlas-search/).
76+
3977
## Adding this module to your project dependencies
4078

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

5795
!!! hint
58-
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
96+
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
5997

6098
#### Copyright
6199
Copyright (c) 2019 Konstantin Silaev <silaev256@gmail.com>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.testcontainers.mongodb;
2+
3+
import org.testcontainers.containers.GenericContainer;
4+
import org.testcontainers.containers.wait.strategy.Wait;
5+
import org.testcontainers.utility.DockerImageName;
6+
7+
/**
8+
* Testcontainers implementation for MongoDB Atlas.
9+
* <p>
10+
* Supported images: {@code mongodb/mongodb-atlas-local}
11+
* <p>
12+
* Exposed ports: 27017
13+
*/
14+
public class MongoDBAtlasLocalContainer extends GenericContainer<MongoDBAtlasLocalContainer> {
15+
16+
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mongodb/mongodb-atlas-local");
17+
18+
private static final int MONGODB_INTERNAL_PORT = 27017;
19+
20+
public MongoDBAtlasLocalContainer(final String dockerImageName) {
21+
this(DockerImageName.parse(dockerImageName));
22+
}
23+
24+
public MongoDBAtlasLocalContainer(final DockerImageName dockerImageName) {
25+
super(dockerImageName);
26+
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
27+
28+
withExposedPorts(MONGODB_INTERNAL_PORT);
29+
waitingFor(Wait.forSuccessfulCommand("runner healthcheck"));
30+
}
31+
32+
/**
33+
* Get the connection string to MongoDB.
34+
*/
35+
public String getConnectionString() {
36+
return String.format("mongodb://%s:%d/?directConnection=true", getHost(), getMappedPort(MONGODB_INTERNAL_PORT));
37+
}
38+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package org.testcontainers.mongodb;
2+
3+
import com.mongodb.ConnectionString;
4+
import com.mongodb.MongoClientSettings;
5+
import com.mongodb.client.ListSearchIndexesIterable;
6+
import com.mongodb.client.MongoClient;
7+
import com.mongodb.client.MongoClients;
8+
import com.mongodb.client.MongoCollection;
9+
import com.mongodb.client.MongoDatabase;
10+
import com.mongodb.client.model.Aggregates;
11+
import com.mongodb.client.model.search.SearchOperator;
12+
import com.mongodb.client.model.search.SearchOptions;
13+
import com.mongodb.client.model.search.SearchPath;
14+
import org.bson.BsonDocument;
15+
import org.bson.Document;
16+
import org.bson.codecs.configuration.CodecRegistries;
17+
import org.bson.codecs.configuration.CodecRegistry;
18+
import org.bson.codecs.pojo.PojoCodecProvider;
19+
import org.bson.conversions.Bson;
20+
import org.bson.json.JsonWriterSettings;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
24+
import java.io.IOException;
25+
import java.net.URISyntaxException;
26+
import java.nio.charset.StandardCharsets;
27+
import java.nio.file.Files;
28+
import java.nio.file.Paths;
29+
import java.time.Instant;
30+
import java.time.temporal.ChronoUnit;
31+
import java.util.Collections;
32+
import java.util.concurrent.TimeUnit;
33+
34+
import static org.awaitility.Awaitility.await;
35+
36+
public class AtlasLocalDataAccess implements AutoCloseable {
37+
38+
private static final Logger log = LoggerFactory.getLogger(AtlasLocalDataAccess.class);
39+
40+
private final MongoClient mongoClient;
41+
42+
private final MongoDatabase testDB;
43+
44+
private final MongoCollection<TestData> testCollection;
45+
46+
private final String collectionName;
47+
48+
public AtlasLocalDataAccess(String connectionString, String databaseName, String collectionName) {
49+
this.collectionName = collectionName;
50+
log.info("DataAccess connecting to {}", connectionString);
51+
52+
CodecRegistry pojoCodecRegistry = CodecRegistries.fromProviders(
53+
PojoCodecProvider.builder().automatic(true).build()
54+
);
55+
CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
56+
MongoClientSettings.getDefaultCodecRegistry(),
57+
pojoCodecRegistry
58+
);
59+
MongoClientSettings clientSettings = MongoClientSettings
60+
.builder()
61+
.applyConnectionString(new ConnectionString(connectionString))
62+
.codecRegistry(codecRegistry)
63+
.build();
64+
mongoClient = MongoClients.create(clientSettings);
65+
testDB = mongoClient.getDatabase(databaseName);
66+
testCollection = testDB.getCollection(collectionName, TestData.class);
67+
}
68+
69+
@Override
70+
public void close() {
71+
mongoClient.close();
72+
}
73+
74+
public void initAtlasSearchIndex() throws URISyntaxException, IOException, InterruptedException {
75+
//Create the collection (if it doesn't exist). Required because unlike other database operations, createSearchIndex will fail if the collection doesn't exist yet
76+
testDB.createCollection(collectionName);
77+
78+
//Read the atlas search index JSON from a resource file
79+
String atlasSearchIndexJson = new String(
80+
Files.readAllBytes(Paths.get(getClass().getResource("/atlas-local-index.json").toURI())),
81+
StandardCharsets.UTF_8
82+
);
83+
log.info(
84+
"Creating Atlas Search index AtlasSearchIndex on collection {}:\n{}",
85+
collectionName,
86+
atlasSearchIndexJson
87+
);
88+
testCollection.createSearchIndex("AtlasSearchIndex", BsonDocument.parse(atlasSearchIndexJson));
89+
90+
//wait for the atlas search index to be ready
91+
Instant start = Instant.now();
92+
await()
93+
.atMost(5, TimeUnit.SECONDS)
94+
.pollInterval(10, TimeUnit.MILLISECONDS)
95+
.pollInSameThread()
96+
.until(this::getIndexStatus, "READY"::equalsIgnoreCase);
97+
98+
log.info(
99+
"Atlas Search index AtlasSearchIndex on collection {} is ready (took {} milliseconds) to create.",
100+
collectionName,
101+
start.until(Instant.now(), ChronoUnit.MILLIS)
102+
);
103+
}
104+
105+
private String getIndexStatus() {
106+
ListSearchIndexesIterable<Document> searchIndexes = testCollection.listSearchIndexes();
107+
for (Document searchIndex : searchIndexes) {
108+
if (searchIndex.get("name").equals("AtlasSearchIndex")) {
109+
return searchIndex.getString("status");
110+
}
111+
}
112+
return null;
113+
}
114+
115+
public void insertData(TestData data) {
116+
log.info("Inserting document {}", data);
117+
testCollection.insertOne(data);
118+
}
119+
120+
public TestData findAtlasSearch(String test) {
121+
Bson searchClause = Aggregates.search(
122+
SearchOperator.of(SearchOperator.text(SearchPath.fieldPath("test"), test).fuzzy()),
123+
SearchOptions.searchOptions().index("AtlasSearchIndex")
124+
);
125+
log.trace(
126+
"Searching for document using Atlas Search:\n{}",
127+
searchClause.toBsonDocument().toJson(JsonWriterSettings.builder().indent(true).build())
128+
);
129+
return testCollection.aggregate(Collections.singletonList(searchClause)).first();
130+
}
131+
132+
public static class TestData {
133+
134+
String test;
135+
136+
int test2;
137+
138+
boolean test3;
139+
140+
public TestData() {}
141+
142+
public TestData(String test, int test2, boolean test3) {
143+
this.test = test;
144+
this.test2 = test2;
145+
this.test3 = test3;
146+
}
147+
148+
public String getTest() {
149+
return test;
150+
}
151+
152+
public void setTest(String test) {
153+
this.test = test;
154+
}
155+
156+
public int getTest2() {
157+
return test2;
158+
}
159+
160+
public void setTest2(int test2) {
161+
this.test2 = test2;
162+
}
163+
164+
public boolean isTest3() {
165+
return test3;
166+
}
167+
168+
public void setTest3(boolean test3) {
169+
this.test3 = test3;
170+
}
171+
172+
@Override
173+
public String toString() {
174+
return "TestData{" + "test='" + test + '\'' + ", test2=" + test2 + ", test3=" + test3 + '}';
175+
}
176+
}
177+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package org.testcontainers.mongodb;
2+
3+
import org.junit.Test;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
7+
import java.time.Instant;
8+
import java.time.temporal.ChronoUnit;
9+
import java.util.Objects;
10+
import java.util.concurrent.TimeUnit;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
import static org.awaitility.Awaitility.await;
14+
15+
public class MongoDBAtlasLocalContainerTest {
16+
17+
private static final Logger log = LoggerFactory.getLogger(MongoDBAtlasLocalContainerTest.class);
18+
19+
@Test
20+
public void getConnectionString() {
21+
try (
22+
MongoDBAtlasLocalContainer container = new MongoDBAtlasLocalContainer("mongodb/mongodb-atlas-local:7.0.9")
23+
) {
24+
container.start();
25+
String connectionString = container.getConnectionString();
26+
assertThat(connectionString).isNotNull();
27+
assertThat(connectionString).startsWith("mongodb://");
28+
assertThat(connectionString)
29+
.isEqualTo(
30+
String.format(
31+
"mongodb://%s:%d/?directConnection=true",
32+
container.getHost(),
33+
container.getFirstMappedPort()
34+
)
35+
);
36+
}
37+
}
38+
39+
@Test
40+
public void createAtlasIndexAndSearchIt() throws Exception {
41+
try (
42+
// creatingAtlasLocalContainer {
43+
MongoDBAtlasLocalContainer atlasLocalContainer = new MongoDBAtlasLocalContainer(
44+
"mongodb/mongodb-atlas-local:7.0.9"
45+
);
46+
// }
47+
) {
48+
// startingAtlasLocalContainer {
49+
atlasLocalContainer.start();
50+
// }
51+
52+
// getConnectionStringAtlasLocalContainer {
53+
String connectionString = atlasLocalContainer.getConnectionString();
54+
// }
55+
56+
try (
57+
AtlasLocalDataAccess atlasLocalDataAccess = new AtlasLocalDataAccess(connectionString, "test", "test")
58+
) {
59+
atlasLocalDataAccess.initAtlasSearchIndex();
60+
61+
atlasLocalDataAccess.insertData(new AtlasLocalDataAccess.TestData("tests", 123, true));
62+
63+
Instant start = Instant.now();
64+
log.info(
65+
"Waiting for Atlas Search to index the data by polling atlas search query (Atlas Search is eventually consistent)"
66+
);
67+
await()
68+
.atMost(5, TimeUnit.SECONDS)
69+
.pollInterval(10, TimeUnit.MILLISECONDS)
70+
.pollInSameThread()
71+
.until(() -> atlasLocalDataAccess.findAtlasSearch("test"), Objects::nonNull);
72+
log.info(
73+
"Atlas Search indexed the new data and was searchable after {}ms.",
74+
start.until(Instant.now(), ChronoUnit.MILLIS)
75+
);
76+
}
77+
}
78+
}
79+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"mappings": {
3+
"dynamic": false,
4+
"fields": {
5+
"test": {
6+
"type": "string"
7+
},
8+
"test2": {
9+
"type": "number",
10+
"representation": "int64",
11+
"indexDoubles": false
12+
},
13+
"test3": {
14+
"type": "boolean"
15+
}
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)