Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Test @Transactional with Flapdoodle Embedded MongoDB in Spring Boot

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.

like image 777
DCO Avatar asked May 19 '20 07:05

DCO


1 Answers

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()
        }
    }
}
like image 181
Python Dev Avatar answered Nov 15 '22 05:11

Python Dev