Tester en Kotlin

Category: 
April 17, 2019

C'est bon, vous avez basculé vers le nouveau langage à la mode Kotlin et vous allez écrire vos premières lignes de code. Hopopop, vous commencez bien par écrire vos tests ? Et là une question se pose : comment j'écris mes tests en Kotlin ? Est-ce qu'il y a des frameworks reconnus dans l'écosystème Kotlin ? Après plusieurs mois de pratique, je peux vous donner des éléments de réponses.

Ecosystème Kotlin

Framework de test

kotlin.test est le framework de test proposé par JetBrains. Il propose plusieurs styles pour écrire ses tests unitaires. Voici quelques exemples :

  • String Spec
class MyTests : StringSpec({
    "strings.length should return size of string" {
      "hello".length shouldBe 5
    }
})
  •  Fun Spec
class MyTests : FunSpec({
    test("String length should return the length of the string") {
        "sammy".length shouldBe 5
        "".length shouldBe 0
    }
})
  • Describe Spec
class MyTests : DescribeSpec({
    describe("score") {
        it("start as zero") {
            // test here
        }
        context("with a strike") {
            it("adds ten") {
                // test here
            }
            it("carries strike to the next frame") {
                // test here
            }
       }
   }
}

Il existe aussi le Behavior Spec (style BDD), le Feature Spec (style Cucumber), ... En fait kotlin.test fait penser à ScalaTest ("mais en moins puissant", dixit mes collègues développeurs Scala)

Dans la même philosophie, on trouve aussi Spek qui propose deux styles de tests :

  • Specification inspiré des tests Jasmine
object CalculatorSpec: Spek({
    describe("A calculator") {
        val calculator by memoized { Calculator() }

        describe("addition") {
            it("returns the sum of its arguments") {
                assertEquals(3, calculator.add(1, 2))
            }
        }
    }
})
  •  Gherkin basé sur un DSL utilisant les mots-clés Given, When, Then
object SetFeature: Spek({
    Feature("Set") {
        val set by memoized { mutableSetOf<String>() }

        Scenario("empty") {
            Then("should have a size of 0") {
                assertEquals(0, set.size)
            }

            Then("should throw when first is invoked") {
                assertFailsWith(NoSuchElementException::class) {
                    set.first()
                }
            }
        }
    }
})

La version 1.0 était dans le repository github de JetBrains, et la version 2.0 a migré vers sa propre organisation (https://github.com/spekframework). Bon ou mauvais signe ?

Que ce soit pour kotlin.test ou Spek, l'idée de proposer plusieurs styles est séduisante. C'est une tendance dans les nouveaux frameworks de tests quel que soit le langage. Mais pour un développeur Java habitué à Junit, c'est un peu perturbant et peut ajouter une marche supplémentaire à l'adoption du langage.

Mocking

Dans l'univers des frameworks de Mocking, le petit dernier qui revient souvent est : mockK. En plus de proposer un ensemble d'annotations pour créer vos mocks et vos spies, il propose un DSL kotlin plutôt élégant et facile à utiliser.

val car = mockkClass(Car::class)

every { car.drive(Direction.NORTH) } returns Outcome.OK

car.drive(Direction.NORTH) // returns OK

verify { car.drive(Direction.NORTH) }

NOTE:  Il existe aussi une extension Spring springmockk permettant de s'intégrer facilement dans un contexte spring : https://github.com/Ninja-Squad/springmockk

Assertions

Il existe plein de librairies d'assertions dans l'écosystème Kotlin !

  • AssertK
  • Kluent
  • Strikt
  • Expekt ...

Je n'ai pas vraiment eu le temps de regarder quelles sont les différences mais elles sont soit inspirées de AssertJ, soit de librairies js comme Chai.js.

Et si on restait avec l'écosystème Java ?

Kotlin est un langage qui proprose une réelle interopérabilité avec java, il est donc intéressant de regarder comment les frameworks classiques de l'écosystème java s'adaptent au langage Kotlin, c'est-à-dire Junit Mockito AssertJ

 Junit

Si vous utilisez encore Junit 4, il va falloir migrer en Junit 5

En effet, le cycle d'instanciation des tests n'est pas du tout adapté à Kotlin : la classe de test est instanciée à chaque méthode de test, par conséquent il est souvent nécessaire d'utiliser des champs statiques, donc des companions en Kotlin. Les tests perdent beaucoup en visibilité .... 

class DAOTestJUnit4 {

    companion object {
        @JvmStatic
        private lateinit var database: Database
        @JvmStatic
        private lateinit var dao: DAO

        @BeforeClass
        @JvmStatic
        fun initialize() {
            database = startDatabase()
            dao = DAO(database.host, database.port)
        }
    }

    @Test
    fun foo() {
        // test DAO
    }
}

 

Si vous passez en Junit 5, tout devient plus simple, car il est possible d'avoir une seule instance de classe pour toutes les méthodes de tests, donc plus besoin de companion, plus de besoin de lateinit var

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DAOTestJUnit5 {
    private val database = startDatabase()
    private val DAO = DAO(database.host, database.port)

    @Test
    fun foo() {
        // test DAO
    }
}

NOTE:  Vous pouvez remplacer l'annotation @TestInstance par un fichier "src/test/resources/junit-platform.properties" avec le contenu

junit.jupiter.testinstance.lifecycle.default = per_class

 

Après vous pouvez écrire vos tests comme d'habitude, et petit à petit vous allez vous apercevoir que les petits plus de Kotlin sont aussi très utiles dans l'écriture des tests ;-)  

Mockito

En Kotlin, les classes et les méthodes sont final par défaut. Malheureusement, Mockito ne peut pas mocker ce qui est final. Plusieurs solutions :

  • utiliser des interfaces pour pouvoir mocker : un peu pénible si le design ne le demande pas
  • utiliser le mot clé open partout pour expliciter que c'est pas final : un peu contre la philosophie du langage

Bref, dans l'état actuel, Mockito et Kotlin ne sont pas de bons amis. Il existe une extension extension non-officielle en cours d'expérimentation (https://github.com/nhaarman/mockito-kotlin) mais je vous recommande plutôt de vous pencher vers MockK.

AssertJ

AssertJ (fork *de* feu fest-assert) fournit un ensemble d'assertions pour presque tous les cas d'utilisation, sous la forme  

assertThat(...).isNotNull().hasSize(5)

Avec Kotlin, tout se passe très bien, et il est toujours aussi élégant d'enchainer les assertions. D'autant plus, qu'en utilisant les data class dans les assertions, les messages d'erreur sont encore plus explicites

val person = dao.findById(id = 1)
val expectedPerson = Person(id = 1, name = "Bob", age= 18)
assertThat(person).isEqualTo(expectedPerson)
org.junit.ComparisonFailure: expected:<Person(id=[1], name=Bob...> but was:<Person(id=[1], name=Alice...>
Expected :Person(id=1, name=Bob, age=18)
Actual   :Person(id=1, name=Alice, age = 21)

Cependant, j'ai rencontré quelques cas en particulier sur des listes où j'ai dû caster assertThat en AbstractAssert car le compilateur n'arrivait pas à résoudre l'inférence de type.

Pour résumer

Personnellement, il y a un an quand j'ai commencé à coder en Kotlin, j'ai choisi la facilité en restant avec mes outils de l'écosystème java. Heureusement j'avais déjà anticipé la migration en Junit 5 ce qui a simplifié grandement l'intégration avec Koltin. Concernant mockito, j'ai progressivement migré vers mockK qui est vraiment très sympa à utiliser avec son DSL. D'ailleurs, j'ai vu pas mal de tweets qui vont dans ce sens comme dans la communauté Kotlin de Spring. Enfin, AssertJ reste pour moi un incontournable pour les assertions malgré quelques petites incompatibilités, qui seront sûrement réglées dans les prochaines versions.

En complément de lecture, je vous propose cet article de blog qui recense plein de trucs et astuces pour améliorer votre environnement de tests : https://phauer.com/2018/best-practices-unit-testing-kotlin/ 

Sources