Skip to content

Commit 8377288

Browse files
authored
Add MongoDB module (#1961)
1 parent 9b12c64 commit 8377288

File tree

6 files changed

+278
-0
lines changed

6 files changed

+278
-0
lines changed

docs/modules/databases/mongodb.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# MongoDB Module
2+
3+
!!! note
4+
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.
5+
6+
## Usage example
7+
8+
The following example shows how to create a MongoDBContainer:
9+
10+
<!--codeinclude-->
11+
[Creating a MongoDB container](../../../modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java) inside_block:creatingMongoDBContainer
12+
<!--/codeinclude-->
13+
14+
And how to start it:
15+
16+
<!--codeinclude-->
17+
[Starting a MongoDB container](../../../modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java) inside_block:startingMongoDBContainer
18+
<!--/codeinclude-->
19+
20+
!!! note
21+
To construct a multi-node MongoDB cluster, consider the [mongodb-replica-set project](https://github.com/silaev/mongodb-replica-set/)
22+
23+
#### Motivation
24+
Implement a reusable, cross-platform, simple to install solution that doesn't depend on
25+
fixed ports to test MongoDB transactions.
26+
27+
#### General info
28+
MongoDB starting from version 4 supports multi-document transactions only for a replica set.
29+
For instance, to initialize a single node replica set on fixed ports via Docker, one has to do the following:
30+
31+
* Run a MongoDB container of version 4 and up specifying --replSet command
32+
* Initialize a single replica set via executing a proper command
33+
* Wait for the initialization to complete
34+
* Provide a special url for a user to employ with a MongoDB driver without specifying replicaSet
35+
36+
As we can see, there is a lot of operations to execute and we even haven't touched a non-fixed port approach.
37+
That's where the MongoDBContainer might come in handy.
38+
39+
## Adding this module to your project dependencies
40+
41+
Add the following dependency to your `pom.xml`/`build.gradle` file:
42+
43+
```groovy tab='Gradle'
44+
testCompile "org.testcontainers:mongodb:{{latest_version}}"
45+
```
46+
47+
```xml tab='Maven'
48+
<dependency>
49+
<groupId>org.testcontainers</groupId>
50+
<artifactId>mongodb</artifactId>
51+
<version>{{latest_version}}</version>
52+
<scope>test</scope>
53+
</dependency>
54+
```
55+
56+
!!! hint
57+
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
58+
59+
#### Copyright
60+
Copyright (c) 2019 Konstantin Silaev <[email protected]>

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ nav:
5050
- modules/databases/dynalite.md
5151
- modules/databases/influxdb.md
5252
- modules/databases/mariadb.md
53+
- modules/databases/mongodb.md
5354
- modules/databases/mssqlserver.md
5455
- modules/databases/mysql.md
5556
- modules/databases/neo4j.md

modules/mongodb/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
description = "Testcontainers :: MongoDB"
2+
3+
dependencies {
4+
compile project(':testcontainers')
5+
6+
testCompile("org.mongodb:mongodb-driver-sync:4.0.2")
7+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package org.testcontainers.containers;
2+
3+
import com.github.dockerjava.api.command.InspectContainerResponse;
4+
import lombok.NonNull;
5+
import lombok.SneakyThrows;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.testcontainers.containers.wait.strategy.Wait;
8+
9+
import java.io.IOException;
10+
11+
/**
12+
* Constructs a single node MongoDB replica set for testing transactions.
13+
* <p>To construct a multi-node MongoDB cluster, consider the <a href="https://github.com/silaev/mongodb-replica-set/">mongodb-replica-set project on GitHub</a>
14+
* <p>Tested on a MongoDB version 4.0.10+ (that is the default version if not specified).
15+
*/
16+
@Slf4j
17+
public class MongoDBContainer extends GenericContainer<MongoDBContainer> {
18+
private static final int CONTAINER_EXIT_CODE_OK = 0;
19+
private static final int MONGODB_INTERNAL_PORT = 27017;
20+
private static final int AWAIT_INIT_REPLICA_SET_ATTEMPTS = 60;
21+
private static final String MONGODB_VERSION_DEFAULT = "4.0.10";
22+
private static final String MONGODB_DATABASE_NAME_DEFAULT = "test";
23+
24+
public MongoDBContainer() {
25+
this("mongo:" + MONGODB_VERSION_DEFAULT);
26+
}
27+
28+
public MongoDBContainer(@NonNull final String dockerImageName) {
29+
super(dockerImageName);
30+
withExposedPorts(MONGODB_INTERNAL_PORT);
31+
withCommand("--replSet", "docker-rs");
32+
waitingFor(
33+
Wait.forLogMessage(".*waiting for connections on port.*", 1)
34+
);
35+
}
36+
37+
public String getReplicaSetUrl() {
38+
if (!isRunning()) {
39+
throw new IllegalStateException("MongoDBContainer should be started first");
40+
}
41+
return String.format(
42+
"mongodb://%s:%d/%s",
43+
getContainerIpAddress(),
44+
getMappedPort(MONGODB_INTERNAL_PORT),
45+
MONGODB_DATABASE_NAME_DEFAULT
46+
);
47+
}
48+
49+
@Override
50+
protected void containerIsStarted(InspectContainerResponse containerInfo) {
51+
initReplicaSet();
52+
}
53+
54+
private String[] buildMongoEvalCommand(final String command) {
55+
return new String[]{"mongo", "--eval", command};
56+
}
57+
58+
private void checkMongoNodeExitCode(final Container.ExecResult execResult) {
59+
if (execResult.getExitCode() != CONTAINER_EXIT_CODE_OK) {
60+
final String errorMessage = String.format("An error occurred: %s", execResult.getStdout());
61+
log.error(errorMessage);
62+
throw new ReplicaSetInitializationException(errorMessage);
63+
}
64+
}
65+
66+
private String buildMongoWaitCommand() {
67+
return String.format(
68+
"var attempt = 0; " +
69+
"while" +
70+
"(%s) " +
71+
"{ " +
72+
"if (attempt > %d) {quit(1);} " +
73+
"print('%s ' + attempt); sleep(100); attempt++; " +
74+
" }",
75+
"db.runCommand( { isMaster: 1 } ).ismaster==false",
76+
AWAIT_INIT_REPLICA_SET_ATTEMPTS,
77+
"An attempt to await for a single node replica set initialization:"
78+
);
79+
}
80+
81+
private void checkMongoNodeExitCodeAfterWaiting(
82+
final Container.ExecResult execResultWaitForMaster
83+
) {
84+
if (execResultWaitForMaster.getExitCode() != CONTAINER_EXIT_CODE_OK) {
85+
final String errorMessage = String.format(
86+
"A single node replica set was not initialized in a set timeout: %d attempts",
87+
AWAIT_INIT_REPLICA_SET_ATTEMPTS
88+
);
89+
log.error(errorMessage);
90+
throw new ReplicaSetInitializationException(errorMessage);
91+
}
92+
}
93+
94+
@SneakyThrows(value = {IOException.class, InterruptedException.class})
95+
private void initReplicaSet() {
96+
log.debug("Initializing a single node node replica set...");
97+
final ExecResult execResultInitRs = execInContainer(
98+
buildMongoEvalCommand("rs.initiate();")
99+
);
100+
log.debug(execResultInitRs.getStdout());
101+
checkMongoNodeExitCode(execResultInitRs);
102+
103+
log.debug(
104+
"Awaiting for a single node replica set initialization up to {} attempts",
105+
AWAIT_INIT_REPLICA_SET_ATTEMPTS
106+
);
107+
final ExecResult execResultWaitForMaster = execInContainer(
108+
buildMongoEvalCommand(buildMongoWaitCommand())
109+
);
110+
log.debug(execResultWaitForMaster.getStdout());
111+
112+
checkMongoNodeExitCodeAfterWaiting(execResultWaitForMaster);
113+
}
114+
115+
public static class ReplicaSetInitializationException extends RuntimeException {
116+
ReplicaSetInitializationException(final String errorMessage) {
117+
super(errorMessage);
118+
}
119+
}
120+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.testcontainers.containers;
2+
3+
import com.mongodb.ReadConcern;
4+
import com.mongodb.ReadPreference;
5+
import com.mongodb.TransactionOptions;
6+
import com.mongodb.WriteConcern;
7+
import com.mongodb.client.ClientSession;
8+
import com.mongodb.client.MongoClient;
9+
import com.mongodb.client.MongoClients;
10+
import com.mongodb.client.MongoCollection;
11+
import com.mongodb.client.TransactionBody;
12+
import org.bson.Document;
13+
import org.junit.Test;
14+
15+
import static org.junit.Assert.assertEquals;
16+
import static org.junit.Assert.assertNotNull;
17+
18+
19+
public class MongoDBContainerTest {
20+
/**
21+
* Taken from <a href="https://docs.mongodb.com/manual/core/transactions/">https://docs.mongodb.com</a>
22+
*/
23+
@Test
24+
public void shouldExecuteTransactions() {
25+
try (
26+
// creatingMongoDBContainer {
27+
final MongoDBContainer mongoDBContainer = new MongoDBContainer()
28+
// }
29+
) {
30+
31+
// startingMongoDBContainer {
32+
mongoDBContainer.start();
33+
// }
34+
35+
final String mongoRsUrl = mongoDBContainer.getReplicaSetUrl();
36+
assertNotNull(mongoRsUrl);
37+
final MongoClient mongoSyncClient = MongoClients.create(mongoRsUrl);
38+
mongoSyncClient.getDatabase("mydb1").getCollection("foo")
39+
.withWriteConcern(WriteConcern.MAJORITY).insertOne(new Document("abc", 0));
40+
mongoSyncClient.getDatabase("mydb2").getCollection("bar")
41+
.withWriteConcern(WriteConcern.MAJORITY).insertOne(new Document("xyz", 0));
42+
43+
final ClientSession clientSession = mongoSyncClient.startSession();
44+
final TransactionOptions txnOptions = TransactionOptions.builder()
45+
.readPreference(ReadPreference.primary())
46+
.readConcern(ReadConcern.LOCAL)
47+
.writeConcern(WriteConcern.MAJORITY)
48+
.build();
49+
50+
final String trxResult = "Inserted into collections in different databases";
51+
52+
TransactionBody<String> txnBody = () -> {
53+
final MongoCollection<Document> coll1 =
54+
mongoSyncClient.getDatabase("mydb1").getCollection("foo");
55+
final MongoCollection<Document> coll2 =
56+
mongoSyncClient.getDatabase("mydb2").getCollection("bar");
57+
58+
coll1.insertOne(clientSession, new Document("abc", 1));
59+
coll2.insertOne(clientSession, new Document("xyz", 999));
60+
return trxResult;
61+
};
62+
63+
try {
64+
final String trxResultActual = clientSession.withTransaction(txnBody, txnOptions);
65+
assertEquals(trxResult, trxResultActual);
66+
} catch (RuntimeException re) {
67+
throw new IllegalStateException(re.getMessage(), re);
68+
} finally {
69+
clientSession.close();
70+
mongoSyncClient.close();
71+
}
72+
}
73+
}
74+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<configuration>
2+
3+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
4+
<!-- encoders are assigned the type
5+
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
6+
<encoder>
7+
<pattern>%d{HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
8+
</encoder>
9+
</appender>
10+
11+
<root level="INFO">
12+
<appender-ref ref="STDOUT"/>
13+
</root>
14+
15+
<logger name="org.testcontainers" level="DEBUG"/>
16+
</configuration>

0 commit comments

Comments
 (0)