DEV Community

Michal Talaga
Michal Talaga

Posted on • Updated on

Integration Tests with Micronaut and Kotlin

MongoDB is really flexible in terms of what can it store. We don't have to tie to specific schema like it is with relational DBs. As our model changes in time, it is not a big deal for Mongo to adjust to it. It makes it easier to design application from the domain perspective, rather than from data perspective. Having this in mind, we would like to store different structures within our table through the code. Official Micronaut Guide is a good place to start, but it took me a while to learn how to store in Mongo objects containing other objects. Here is the solution.

Foundation

In this case I will extend a bit application I've presented in my previous post on integration testing. I would like to create dictionary structure, which will hold word in one language together with its translations. To achieve that I've prepared following structure:

data class Word(val word: String, val translations: List<Translation>)  
data class Translation(val language: String, val translation: String)
Enter fullscreen mode Exit fullscreen mode

If we would like to store it within relational database, by default it would require two tables - one for Word rows, and one for Translation rows with reference to particular Word. Mongo by default allow us to both of these objects within one table. It will contain a Word together with list of Translation in JSON format as a separate field.

In terms of Mongo, above setup is easily achievable. We can create table with two fields word and translations where first one will be string value, and the latter one will be JSON containing list of objects having language and translation fields.

Serde

Micronaut comes with dedicated plugin called micronaut-serde-processor enabling us to serialize and deserialize. We can annotate class with @Serdeable annotation to mark it as the one which will be exchanged in the future. As we are not using micronaut-data which can make things easier (but I was not able to achieve such nested serialization) we will need to rely on manual poinitng how to serialize to and from BSON fields used by Mongo. To enable classes being manipulated such way, we also need to add @Introspected annotation.

As mentioned previously, we will have to point out how to convert our entities. The easiest way would be to do that through the constructor. For it to work, we need to mark our constructor with @Creator and @BsonCreator annotations. Our entity will be converted through the constructor, containing all required fields. For proper conversion, we need as well to show which fields will be taken into consideration. Each one of them needs to be annotated by @field:BsonProperty("name") and @param:BsonProperty("name") annotations. This is to mark the property as both class (field) and constructor(param) properties. Having such prepared class, we do not have to worry about declaration of setters and getters being by default key for serialization process. Our classes will look like that:

  • MongoWord
@Introspected  
@Serdeable  
data class MongoWord @Creator @BsonCreator constructor(  
    @field:BsonProperty("word") @param:BsonProperty("word") val word: String,  
    @field:BsonProperty("translations") @param:BsonProperty("translations") val translations: List<MongoTranslation>  
)
Enter fullscreen mode Exit fullscreen mode
  • MongoTranslation
@Introspected  
@Serdeable  
data class MongoTranslation @Creator @BsonCreator constructor(  
    @field:BsonProperty("language") @param:BsonProperty("language") val language: String,  
    @field:BsonProperty("translation") @param:BsonProperty("translation") val translation: String  
)
Enter fullscreen mode Exit fullscreen mode

Separation of Domain and Entity

It is good practice to separate classes used within our domain logic from the ones being used to communicate with outer world. I like the ability to quickly convert each way eg. through static factory method. In Kotlin we can achieve that using companion object. Such object will look like following for our Word class:

companion object {  
    fun fromWord(word: Word): MongoWord {  
        return MongoWord(word.word, word.translations.map { MongoTranslation.fromTranslation(it) })  
    }  
}
Enter fullscreen mode Exit fullscreen mode

When we want to create domain object straight out of our entity, we can use method being executed on the instance

fun toWord(): Word {  
    return Word(word, translations.map { it.toTranslation() })  
}
Enter fullscreen mode Exit fullscreen mode

Having this methods within transport classes will allow us to hide implementation details from domain Word and Translation object. Thanks to this we can focus on actual business logic, without thinking how our objects should be serialized and deserialized.

Repository

Having everything prepared there is nothing else than building an repository. This will be a Singleton, which will accept and return Word object. As fields used to build it we need MongoClient together with names of database and collection which we will operate on. Then all we have to do, is to implement methods responsible for storing and getting Words from repository. Below is code showing how we can achieve that.

@Singleton  
class WordRepositoryMongo(  
    mongoClient: MongoClient,  
    @Property(name = "word.database") databaseName: String,  
    @Property(name = "word.collection") collectionName: String  
) : WordRepository {  

    private val collection: MongoCollection<MongoWord>  

    init {  
        val db = mongoClient.getDatabase(databaseName)  
        collection = db.getCollection(collectionName, MongoWord::class.java)  
    }  

    override fun findWord(word: String): Word? {  
        return collection.find(eq("word", word)).firstOrNull()?.toWord()  
    }  

    override fun putWord(word: Word) {  
        collection.insertOne(MongoWord.fromWord(word))  
    }  
}
Enter fullscreen mode Exit fullscreen mode

Tests

Testcontainers are really powerful tool which allows us to test all the code we have just written against actual MongoDB instance. Thanks to micronaut io.micronaut.test-resources plugin, the only thing we need to do is to provide dependency to Testcontainers and everything would plug in out-of-the-box. No configuration needed. Before writing test, we need to make sure that with each execution the DB state will be cleared. To do this, we can do following:

@BeforeEach  
fun beforeEach() {  
    mongoClient.getDatabase(databaseName)  
        .getCollection(collectionName)  
        .deleteMany(Document())  
}
Enter fullscreen mode Exit fullscreen mode

It uses injected MongoClient like we use it in WordRepositoryMongo class. From collection declared as class field, we will delete all existing documents. When we have it prepared, then we can execute the sample test.

@Test  
fun shouldStoreWordInRepository() {  
    //Given  
    val word = Word(  
        "hello", listOf(  
            Translation("polish", "czesc"),  
            Translation("deutsch", "hallo")  
        )  
    )  

    //When  
    repository.putWord(word)  

    //Then  
    val wordFromRepository = repository.findWord("hello")  
    Assertions.assertTrue(wordFromRepository != null)  
    Assertions.assertTrue(wordFromRepository!!.translations.size == 2)  
    Assertions.assertTrue(wordFromRepository!!.translations  
        .filter { it.language == "polish" && it.translation == "czesc" }  
        .size == 1)  
    Assertions.assertTrue(wordFromRepository!!.translations  
        .filter { it.language == "deutsch" && it.translation == "hallo" }  
        .size == 1)  
}
Enter fullscreen mode Exit fullscreen mode

It tests if word put could be reached out later.

Conclusion

It was not an easy job for me to find how to store object structure as parameter for Mongo table. Micronaut is still not so popular as Spring, so the community support is not yet so active. I hope that this article could help you design tables which will realise full potential of domain, without need to think about configuration nitpicks.

All the code used in this article you can find here within hello package.

Top comments (0)