😍 Tester en Kotlin

Tu connais ?

kotlin

⏱Kotlin, en quelques dates

  • 2010 💡 JetBrains

  • 2012 🤝 Open Source

  • 2016 🚀 Kotlin 1.0

  • 2017 📱 Google I/O

  • 2019 🤩 Kotlin 1.3.50

🤓Kotlin en quelques mots

Langage statiquement typé, orienté object et fonctionnel, centré sur la productivité du développeur

Kotlin en quelques lignes de code

fun main() {
    println("Hello World!")
}

Forces du langage

  • 💪 Null Safety

  • 👍 Immutable first

  • 📖 Concis et expressif

  • 🤝 Interopabilité avec Java

Kotlin en 2019

kotlin 2019 1

Kotlin en 2019

kotlin 2019 2

Kotlin en 2019

kotlin 2019 3

val me = "Benoit Prioux"

binout / ❤️ Asciidoctor (🐜)

lectra

lectra versalis

{ java → kotlin }

java kotlin

Pourquoi parler des tests ?

testerdouter

🎮 Un super terrain de jeu

  • 😇 Pas du code de production

  • 😜 Plus de liberté dans le design

  • 🧔 Souvent un peu de code barbant

    • préparation des jeux de données

💡Ma proposition

Apprendre Kotlin par les tests
Introduire Kotlin dans votre projet par les tests

Java + Kotlin

java kotlin compilation

Projet mix Java/Kotlin

  1. Créer src/test/kotlin

  2. Configurer build → plugin Maven/Gradle

  3. Ajouter

    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-stdlib</artifactId>
        <version>${kotlin.version}</version>
    </dependency>

Java 🤝 Kotlin

kotlin java

📣Disclaimer

Au cours de cette présentation, je vais aborder des concepts du langage Kotlin par des exemples issus de code de test

🏭Framework de test

  • Un runner

  • Une librairie d’assertions

  • Une librairie de mock

KotlinTest

kotlintest

  • Inspiré de ScalaTest

  • Propose plusieurs styles de test : StringSpec, FunSpec, DescribeSpec, …​

Example

class MyTests : StringSpec({
  "length should return size of string" {
    "hello".length shouldBe 5
  }
  "startsWith should test for a prefix" {
    "world" should startWith("wor")
  }
})

Mon avis

  • Apprendre un nouveau framework de test en plus d’apprendre un nouveau langage 🤯

junit

Junit 4 & Java

public class RepoTestJUnit4 {

    private static Database db;
    private static Repository repo;

    @BeforeClass
    public void initialize() {
        db = new TestDatabase()
        db.start()
        repo = new Repository(db)
    }

    @Test
    public void shouldFindJohn() {
        // test repo
    }
}

Junit 4 & Kotlin

class RepoTestJUnit4 {

    companion object {
        @JvmStatic
        private lateinit var db: Database
        @JvmStatic
        private lateinit var repo: Repository

        @BeforeClass
        @JvmStatic
        fun initialize() {
            db = TestDatabase()
            db.start()
            repo = Repository(db.host, db.port)
        }
    }

    @Test
    fun shouldFindJohn() {
        // test repo
    }
}

WTF ?

wtf

Kotlin, quelques notions 🤓

orelsan

Kotlin val, var

val 👍
val language: String = "Java"
language = "Kotlin" //Compilation error
var 😒
var age: Int = 37
age = 38

Kotlin et null

val language: String = null //Compilation error
val language: String? = null // ✅
var age: Int? = null
age = 38
age = null

Classe en Kotlin

💪
class Person(val name: String, val gender: String)
  • public par défaut

  • déclaration constructeur en même temps que les propriétés

Kotlin : No static

  • 😇 Pas de mot-clé static en Kotlin

  • companion object = singleton associé à une classe

    • permet de définir des fonctions sur cet unique instance rattaché à une classe

Exemple companion object

class Person private constructor(val name: String,
                                 val gender: String) {

    companion object {
        fun man(name: String) = Person(name, "M")
        fun woman(name: String) = Person(name, "F")
    }
}

val john = Person.man("John")
val amanda = Person.woman("Amanda")

Kotlin idiomatic

idiom

Junit 4 & Kotlin

class RepoTestJUnit4 {

    companion object {
        @JvmStatic
        private lateinit var db: Database
        @JvmStatic
        private lateinit var repo: Repository

        @BeforeClass
        @JvmStatic
        fun initialize() {
            db = TestDatabase()
            db.start()
            repo = Repository(db.host, db.port)
        }
    }

    @Test
    fun shouldFindJohn() {
        // test repo
    }
}

Junit 4 : Lifecycle

class RepoTestJUnit4 {

    // Exécuter pour chaque test 🔥
    private val db: Database = ...
    private val repo: Repository = ...

    // 🤯 Où mettre le code du @BeforeClass ?

    @Test
    fun shouldFindJohn() {
        // test repo
    }
}

JUnit 5 FTW !

junit5
  • Refonte totale avec financement participatif 🤝

  • Nouvelle architecture 😎 : annotations, extensions

🤩Junit 5 : Lifecycle

Une seule instance pour tous les tests
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class RepoTestJUnit5 {

    private val db = TestDatabase().apply { start() }
    private val repo = Repository(db.host, db.port)

    @Test
    fun shouldFindJohn() {
        // test repo
    }
}

💡Tips Junit 5

  • Changer le cycle de vie par défaut

src/test/resources/junit-platform.properties
junit.jupiter.testinstance.lifecycle.default = per_class

🤩Junit 5 : Lifecycle

Une seule instance pour tous les tests du projet
class RepoTestJUnit5 {

    private val db = TestDatabase().apply { start() }
    private val repo = Repository(db.host, db.port)

    @Test
    fun shouldFindJohn() {
        // test repo
    }
}

Nommage des tests

🐪 Camel-case
@Test
fun shouldFindPersonIfQueryContainsName() { ...}
〰 Snake-case
@Test
fun should_find_person_if_query_contains_name() { ...}

💡Tips en kotlin

backtick
@Test
fun `should find person if query contains name`() { ...}

Nested inner class

class RepoTestJUnit5 {

    private val db = TestDatabase().apply { start() }
    private val repo = Repository(db.host, db.port)

    @Nested
    inner class QueryTest {
        @Test
        fun `should find person if query contains name`() { ...}
    }

    @Nested
    inner class UpdateTest {
        @Test
        fun `should update person address`() { ...}
    }
}

Nested inner class

nested test

👴Junit 4 & Java

public class RepoTestJUnit4 {

    private static Database db;
    private static Repository repo;

    @BeforeClass
    public void initialize() {
        db = new TestDatabase()
        db.start()
        repo = new Repository(db)
    }

    @Test
    public void shouldFindJohn() {
        // test repo
    }
}

😱Junit 4 & Kotlin

class RepoTestJUnit4 {

    companion object {
        @JvmStatic
        private lateinit var db: Database
        @JvmStatic
        private lateinit var repo: Repository

        @BeforeClass
        @JvmStatic
        fun initialize() {
            db = TestDatabase()
            db.start()
            repo = Repository(db.host, db.port)
        }
    }

    @Test
    fun shouldFindJohn() {
        // test repo
    }
}

🤩Junit 5 & Kotlin idiomatic

class RepoTestJUnit5 {

    private val db = TestDatabase().apply { start() }
    private val repo = Repository(db.host, db.port)

    @Test
    fun `should find John`() {
        // test repo
    }
}

Conclusion

Kotlin ❤️ Junit 5

Assertions

assert

AssertJ

  • 🏟 fork de fest-assert

  • fluent assertions pour (presque) tous les types

assertThat(person.getName()).isEqualTo("John Doe");

🌎Ecosystème Kotlin ?

  • AssertK

  • Strikt : expectThat(..)

🗣

AssertJ reste la référence pour les assertions

Tip AssertJ

😤
assertThat(personDTO.firstName).isEqual("John")
assertThat(personDTO.lastName).isEqual("Doe")
assertThat(personDTO.age).isEqual(37)
assertThat(personDTO.gender).isEqual("M")
Output 🤨
org.junit.ComparisonFailure: expected:<[37]> but was:<[36]>
Expected :37
Actual   :36

Data Class 💡

data class PersonDTO(val firstName: String,
                     val lastName: String,
                     val age:Int,
                     val gender: String)
  • ✅ toString()

  • ✅ equals(), hashcode()

  • ✅ copy()

Equivalent en Java

public class PersonDTO {

    private final String firstName;
    private final String lastName;
    private final int age;
    private final String gender;

    public PersonDTO(String firstName, String lastName,
                     int age, String gender) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.gender = gender;
    }

    public String getFirstName() { return firstName;}
    public String getLastName() { return lastName;}
    public int getAge() { return age;}
    public String getGender() { return gender;}

    @Override
    public String toString() { ... }
    @Override
    public boolean equals(Object obj) { ... }
    @Override
    public int hashCode() { ... }
}

Data Class & AssertJ

val expected = PersonDTO("John", "Doe", 37, "M")

assertThat(personDTO).isEqual(expected)
Output 👍
org.junit.ComparisonFailure:
Expected :PersonDTO(firstName=John, lastName=Doe, age=37, gender=M)
Actual   :PersonDTO(firstName=John, lastName=Doe, age=36, gender=M)

Group assertions with with

with(personDTO) {
    assertThat(firstName).isEqual("John")
    assertThat(lastName).isEqual("Doe")
    assertThat(age).isEqual(37)
    assertThat(gender).isEqual("M")
}

🤓 with under the hood

with(personDTO) {
    assertThat(this.firstName).isEqual("John")
    assertThat(this.lastName).isEqual("Doe")
    assertThat(this.age).isEqual(37)
    assertThat(this.gender).isEqual("M")
}

🤓 with under the hood

with(personDTO, {
    assertThat(this.firstName).isEqual("John")
    assertThat(this.lastName).isEqual("Doe")
    assertThat(this.age).isEqual(37)
    assertThat(this.gender).isEqual("M")
} )

🤓 with under the hood

with → 2 paramètres :

  1. Le receiver

  2. Une fonction avec comme contexte le receiver

😝 Autopromo

BDX I/O 2018 : Dessine moi un DSL en Kotlin

Conclusion

Kotlin ❤️ AssertJ

Mock and co

mock

👻 Mockito ?

mockito

  • ⚠️ when mot clé réservé en Kotlin

  • 😭 mockito ne sait pas mocker les classes finales

    • incubating extensions : mock-maker-inline, mockito-kotlin

👍 Kotlin et final

  • Les classes sont finales par défaut

  • Il faut utiliser le mot-clé open pour explicitement autoriser l’héritage

Mockito et Kotlin ?

Plusieurs solutions pour les classes que l’on veut mocker :

  1. 🤨 Ajouter le mot-clé open

  2. 😩 Définir des interfaces

Alternative 👻 Mockk

Syntaxe DSL élégante avec toutes les fonctions de Mockito

mockk

Exemple

Mockito
`when`(repo.findByName("John")).thenReturn(Person("John", "Doe"))
Mockk
every { repo.findByName("John") } returns Person("John", "Doe")

Spring : Mockito → Mockk

spring mockk

Mockk et Junit5

class PhoneBookTest {

    private var repo: PersonRepository? = null
    private var phoneBook: PhoneBook? = null

    @BeforeEach
    fun init() {
        repo = mockk()
        phoneBook = PhoneBook(repo)
    }

    @Test
    fun `should list contacts`() {
        every { repo!!.findByName("John") } returns Person("John", "Doe")
        ...
    }

}

Mockk et Junit5 : lateinit

class PhoneBookTest {

    private lateinit var repo: PersonRepository
    private lateinit var phoneBook: PhoneBook

    @BeforeEach
    fun init() {
        repo = mockk()
        phoneBook = PhoneBook(repo)
    }

    @Test
    fun `should list contacts`() {
        every { repo.findByName("John") } returns Person("John", "Doe")
        ...
    }

}

lateinit var

  • 🤓 Permet d’indiquer que la propriété sera initialisé par quelqu’un (framework d’injection) avant son utilisation

  • ⚠️ Lance une exception lors de l’accès si pas initialisé

  • 💡 Permet d’éviter d’utiliser un type nullable

Créer des mocks, ça coûte 💵

  • Il faut éviter l’instanciation d’un mock avant chaque test

    • 🐌 ça ralentit les tests !

    • jusqu’à 500 ms pour le 1er mock

La solution

😎 clearMocks()
class PhoneBookTest {

    private val repo = mockk<PersonRepository>()
    private val phoneBook = PhoneBook(repo)

    @BeforeEach
    fun init() {
        clearMocks(repo)
    }

    @Test
    fun `should list contacts`() {
        every { repo.findByName("John") } returns Person("John", "Doe")
        ...
    }

}

Conclusion

Kotlin 💔 Mockito
Kotlin ❤️ Mockk

One more thing 😎

Kotlin String : Template & Raw
val age = 42
val json = """
    {
        "firstName" : "John",
        "lastName" : "Doe"
        "age" : $age
    }
"""

One more AWESOME thing 🤩

Domain Driven Design Style
data class SizeOption(val value:String)

data class ComplexSize(
    val grading: SizeOption,
    val specialGrading1: SizeOption? = null,
    val specialGrading2: SizeOption? = null)

One more AWESOME thing 🤩

Jeu de données en test 😤
val size1 = ComplexSize(
    grading = SizeOption("38"),
    specialGrading1 = SizeOption("M")
)

val size2 = ComplexSize(
    grading = SizeOption("40"),
    specialGrading1 = SizeOption("S"),
    specialGrading2 = SizeOption("30")
)

val size3 = ComplexSize(
    grading = SizeOption("42"),
    specialGrading2 = SizeOption("40")
)

One more AWESOME thing 🤩

Extension Function
fun String.toSize() :Size {
    // `this` is the current instance of String
 }

val size1 = "38".toSize()
val size2 = "40,S,30".toSize()
val size3 = "42,,40".toSize()

One more AWESOME thing 🤩

Extension Function
fun String.toSize() :Size {
    val s = this.split(",")
    return when (s.size) {
        1 -> Size(SizeOption(s[0]))

        2 -> Size(SizeOption(s[0]),
                  SizeOption(s[1]), null)

        3 -> Size(SizeOption(s[0]),
                  if (s[1].length == 0) null else SizeOption(s[1]),
                  SizeOption(s[2]))

        else -> throw IllegalArgumentException("Bad size format : $s")
    }
}

En résumé

summary

Alors convaincu ?

convinced

Retour d’expérience

  • 🏭Projet actuel (2 ans et 1/2)

    • 7 microservices en Spring Boot

    • ☕️ 50 000 LOC en Java

  • Depuis 1 an

    • 🤓 Apprentissage puis migration Kotlin

Retour d’expérience

  • 🤩 Aujourd’hui

    • 5 microservices en full Kotlin

    • 2 autres en cours de migration

  • 35 000 LOC en tout

    • ☕️10 000 en Java

    • 👻25 000 en Kotlin

LE chiffre

loc

Et sans perte de lisibilité !

  • dataclass (ValueObject en DDD)

  • Boilerplate code 📉

Java Stream
persons.stream()
    .map(p -> p.getName() + "-" + p.getAge())
    .collect(Collectors.toList())
Kotlin
persons.map { "${it.name}-${it.age}" }

Lundi matin 👇

  • Pré-requis: Junit 5, Intellij

  • 🚀 Etapes

    1. Configurer maven/gradle pour Kotlin

    2. Créer src/test/kotlin

    3. Ajouter la dépendance mockk

Sinon …​ 😉

Pour aller plus loin

Merci !