Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Michal Talaga
Michal Talaga

Posted on • Edited 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 onintegration 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 modeExit fullscreen mode

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

In terms of Mongo, above setup is easily achievable. We can create table with two fieldsword andtranslations where first one will be string value, and the latter one will be JSON containing list of objects havinglanguage andtranslation fields.

Serde

Micronaut comes with dedicated plugin calledmicronaut-serde-processor enabling us toserialize anddeserialize. We can annotate class with@Serdeable annotation to mark it as the one which will be exchanged in the future. As we are not usingmicronaut-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 fromBSON 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 modeExit 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 modeExit 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. throughstatic factory method. In Kotlin we can achieve that usingcompanion object. Such object will look like following for ourWord class:

companion object {      fun fromWord(word: Word): MongoWord {          return MongoWord(word.word, word.translations.map { MongoTranslation.fromTranslation(it) })      }  }
Enter fullscreen modeExit 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 modeExit fullscreen mode

Having this methods within transport classes will allow us to hide implementation details from domainWord andTranslation 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 returnWord object. As fields used to build it we needMongoClient 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 gettingWords 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 modeExit 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 micronautio.micronaut.test-resources plugin, the only thing we need to do is to provide dependency toTestcontainers 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 modeExit fullscreen mode

It uses injectedMongoClient like we use it inWordRepositoryMongo 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 modeExit 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 findhere withinhello package.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Software engineer currently involved in Java projects
  • Location
    Poland
  • Joined

More fromMichal Talaga

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp