I want to ensure that @Transactional annotation works so I wrote a test which save and publish article - my kafka publisher is a mock which throws an exception on any call. I want to ensure MongoDB rolls back the persisted article.
@Test
void testRollbackOnPublishFail() {
when(producer.publishArticle(any())).thenThrow(IllegalStateException.class);
ArticleDocument articleDocument = ArticleTestDataUtil.createArticleDocument();
try {
ArticleDocument publishedDocument = articleService.saveAndPublish(articleDocument);
} catch (Exception e) {
assertTrue(e instanceof IllegalStateException);
}
assertFalse(articleService.findById(articleDocument.getId()).isPresent());
}
I am using flapdoodles embedded mongo db for integration tests
testCompile "de.flapdoodle.embed:de.flapdoodle.embed.mongo:2.2.0"
This tests fails because there is no transaction / replication on default.
So activated transactions by creating MongoTransactionManager:
@Configuration
public class MongoTransactionConfig {
@Bean
public MongoTransactionManager transactionManager(MongoDbFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
}
Now my test fails because was not able to start a Session in MongoClient
com.mongodb.MongoClientException: Sessions are not supported by the MongoDB cluster to which this client is connected
at com.mongodb.MongoClient.startSession(MongoClient.java:560)
I also tried to create a custom IMongodConfig
@Bean(name = "customReplicaMongodConfig")
public IMongodConfig mongodConfig(EmbeddedMongoProperties embeddedProperties) throws IOException {
Storage storage = new Storage("/tmp", "rs0", 0);
return new MongodConfigBuilder()
.shardServer(true)
.version(Version.V4_0_2)
.net(new Net(27117, Network.localhostIsIPv6()))
.replication(storage)
.cmdOptions(new MongoCmdOptionsBuilder().useNoJournal(false).build()).build();
}
And initiate the replication:
@ConditionalOnBean(name = "customReplicaMongodConfig")
@Configuration
public class ReplicaConfig {
@Inject
private MongoClient mongoClient;
@PostConstruct
public void initiateReplicationSet() {
mongoClient.getDatabase("admin").runCommand(new Document("replSetInitiate", new Document()));
}
}
But the replSetInitiate failed with an timeout.
So my question is if it is possible to create a running replication set with embedded MongoDB to test transactional.
You can find information about creating replica set here
My Kotlin solution:
import com.mongodb.BasicDBList
import com.mongodb.BasicDBObjectBuilder
import com.mongodb.DBObject
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import com.mongodb.client.MongoCollection
import com.mongodb.client.MongoDatabase
import de.flapdoodle.embed.mongo.MongodExecutable
import de.flapdoodle.embed.mongo.MongodProcess
import de.flapdoodle.embed.mongo.MongodStarter
import de.flapdoodle.embed.mongo.config.MongoCmdOptionsBuilder
import de.flapdoodle.embed.mongo.config.MongodConfigBuilder
import de.flapdoodle.embed.mongo.config.Net
import de.flapdoodle.embed.mongo.distribution.Version
import de.flapdoodle.embed.process.runtime.Network
import org.assertj.core.api.Assertions.assertThat
import org.bson.Document
import org.junit.jupiter.api.Test
import org.springframework.data.mongodb.MongoDatabaseFactory
import org.springframework.data.mongodb.MongoTransactionManager
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory
import org.springframework.test.context.ActiveProfiles
import org.springframework.transaction.TransactionStatus
import org.springframework.transaction.support.TransactionCallbackWithoutResult
import org.springframework.transaction.support.TransactionTemplate
import java.io.IOException
@ActiveProfiles("test")
class EmbeddedMongoDbTransactionTest {
private val CONNECTION_STRING = "mongodb://%s:%d/"
private var node1MongodExe: MongodExecutable? = null
private var node1Mongod: MongodProcess? = null
private var mongo: MongoClient? = null
private var node2MongodExe: MongodExecutable? = null
private var node2Mongod: MongodProcess? = null
@Test
@Throws(IOException::class)
fun testSmth() {
val runtime = MongodStarter.getDefaultInstance()
val node1Port = 57023
val node2Port = 57024
try {
node1MongodExe = runtime.prepare(
MongodConfigBuilder().version(Version.Main.PRODUCTION)
.withLaunchArgument("--replSet", "rs0")
.cmdOptions(MongoCmdOptionsBuilder().useNoJournal(false).build())
.net(Net(node1Port, Network.localhostIsIPv6())).build()
)
node1Mongod = node1MongodExe?.start()
node2MongodExe = runtime.prepare(
MongodConfigBuilder().version(Version.Main.PRODUCTION)
.withLaunchArgument("--replSet", "rs0")
.cmdOptions(MongoCmdOptionsBuilder().useNoJournal(false).build())
.net(Net(node2Port, Network.localhostIsIPv6())).build()
)
node2Mongod = node2MongodExe?.start()
mongo = MongoClients.create(CONNECTION_STRING.format("localhost", node1Port))
val adminDatabase: MongoDatabase = mongo!!.getDatabase("admin")
val config = Document("_id", "rs0")
val members = BasicDBList()
members.add(Document("_id", 0).append("host", "localhost:$node1Port"))
members.add(Document("_id", 1).append("host", "localhost:$node2Port"))
config.put("members", members)
adminDatabase.runCommand(Document("replSetInitiate", config))
println(">>>>>> wait")
println(">>>>>>>>" + adminDatabase.runCommand(Document("replSetGetStatus", 1)))
Thread.sleep(15_000) // without waiting fails with error : 'not master' on server
val funDb: MongoDatabase = mongo?.getDatabase("fun")!!
// insert test 1
val testCollection: MongoCollection<Document> = funDb.getCollection("test")
println(">>>>>>>> inserting data")
testCollection.insertOne(Document("fancy", "value"))
println(">>>>>>>> finding data")
assertThat(testCollection.find().first()!!.get("fancy")).isEqualTo("value")
// insert test 2 (with transaction)
val mongoTemplate = MongoTemplate(mongo!!, "test")
// Without creating collection in advance fails with error:
// Cannot create namespace in multi-document transaction
// (https://stackoverflow.com/questions/52585715/cannot-create-namespace-in-multi-document-transactionmongodb-4-0-spring-data-2)
mongoTemplate.createCollection("collection")
val mongoDatabaseFactory: MongoDatabaseFactory = SimpleMongoClientDatabaseFactory(mongo!!, "test")
val mongoTransactionManager = MongoTransactionManager(mongoDatabaseFactory)
val transactionTemplate = TransactionTemplate(mongoTransactionManager)
transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
override fun doInTransactionWithoutResult(status: TransactionStatus) {
val objectToSave = BasicDBObjectBuilder.start()
.add("key", "value")
.get()
// when
mongoTemplate.save(objectToSave, "collection")
// then
assertThat(mongoTemplate.findAll(DBObject::class.java, "collection"))
.extracting("key")
.containsOnly("value")
}
})
// after transaction
assertThat(mongoTemplate.findAll(DBObject::class.java, "collection"))
.extracting("key")
.containsOnly("value")
} finally {
println(">>>>>> shutting down")
mongo?.close()
node1MongodExe?.stop()
node1Mongod?.stop()
node2MongodExe?.stop()
node2Mongod?.stop()
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With