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()
}
}
}
}
})
Mocking
val car = mockkClass(Car::class)
every { car.drive(Direction.NORTH) } returns Outcome.OK
car.drive(Direction.NORTH) // returns OK
verify { car.drive(Direction.NORTH) }
Assertions
AssertK Kluent Strikt Expekt ...
Et si on restait avec l'écosystème Java ?
Junit
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
}
}
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
assertThat(...).isNotNull().hasSize(5)
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)
Pour résumer
Sources
kotin.test : https://kotlinlang.org/api/latest/kotlin.test/index.html speck : https://spekframework.org/ Mockk : https://github.com/mockk/mockk assertk : https://github.com/willowtreeapps/assertk strikt : https://strikt.io/ springMockk : https://github.com/Ninja-Squad/springmockk Junit5 : https://junit.org/junit5/ mockito : https://site.mockito.org/ assertJ : http://joel-costigliola.github.io/assertj/