2010 💡 JetBrains
2012 🤝 Open Source
2016 🚀 Kotlin 1.0
2017 📱 Google I/O
2019 🤩 Kotlin 1.3.50
Langage statiquement typé, orienté object et fonctionnel, centré sur la productivité du développeur
fun main() {
println("Hello World!")
}
💪 Null Safety
👍 Immutable first
📖 Concis et expressif
🤝 Interopabilité avec Java
binout / ❤️ Asciidoctor (🐜)
😇 Pas du code de production
😜 Plus de liberté dans le design
🧔 Souvent un peu de code barbant
préparation des jeux de données
Apprendre Kotlin par les tests
Introduire Kotlin dans votre projet par les tests
Créer src/test/kotlin
Configurer build → plugin Maven
/Gradle
Ajouter
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
Au cours de cette présentation, je vais aborder des concepts du langage Kotlin par des exemples issus de code de test
Un runner
Une librairie d’assertions
Une librairie de mock
Inspiré de ScalaTest
Propose plusieurs styles de test : StringSpec
, FunSpec
, DescribeSpec
, …
class MyTests : StringSpec({
"length should return size of string" {
"hello".length shouldBe 5
}
"startsWith should test for a prefix" {
"world" should startWith("wor")
}
})
Apprendre un nouveau framework de test en plus d’apprendre un nouveau langage 🤯
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
}
}
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
}
}
val
, var
val language: String = "Java"
language = "Kotlin" //Compilation error
var age: Int = 37
age = 38
val language: String = null //Compilation error
val language: String? = null // ✅
var age: Int? = null
age = 38
age = null
class Person(val name: String, val gender: String)
public par défaut
déclaration constructeur en même temps que les propriétés
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
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")
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
}
}
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
}
}
Refonte totale avec financement participatif 🤝
Nouvelle architecture 😎 : annotations, extensions
@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
}
}
Changer le cycle de vie par défaut
junit.jupiter.testinstance.lifecycle.default = per_class
class RepoTestJUnit5 {
private val db = TestDatabase().apply { start() }
private val repo = Repository(db.host, db.port)
@Test
fun shouldFindJohn() {
// test repo
}
}
@Test
fun shouldFindPersonIfQueryContainsName() { ...}
@Test
fun should_find_person_if_query_contains_name() { ...}
@Test
fun `should find person if query contains name`() { ...}
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`() { ...}
}
}
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
}
}
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
}
}
class RepoTestJUnit5 {
private val db = TestDatabase().apply { start() }
private val repo = Repository(db.host, db.port)
@Test
fun `should find John`() {
// test repo
}
}
Kotlin ❤️ Junit 5
🏟 fork de fest-assert
fluent assertions pour (presque) tous les types
assertThat(person.getName()).isEqualTo("John Doe");
AssertK
Strikt : expectThat(..)
🗣
AssertJ
reste la référence pour les assertions
assertThat(personDTO.firstName).isEqual("John")
assertThat(personDTO.lastName).isEqual("Doe")
assertThat(personDTO.age).isEqual(37)
assertThat(personDTO.gender).isEqual("M")
org.junit.ComparisonFailure: expected:<[37]> but was:<[36]>
Expected :37
Actual :36
data class PersonDTO(val firstName: String,
val lastName: String,
val age:Int,
val gender: String)
✅ toString()
✅ equals(), hashcode()
✅ copy()
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() { ... }
}
val expected = PersonDTO("John", "Doe", 37, "M")
assertThat(personDTO).isEqual(expected)
org.junit.ComparisonFailure:
Expected :PersonDTO(firstName=John, lastName=Doe, age=37, gender=M)
Actual :PersonDTO(firstName=John, lastName=Doe, age=36, gender=M)
with
with(personDTO) {
assertThat(firstName).isEqual("John")
assertThat(lastName).isEqual("Doe")
assertThat(age).isEqual(37)
assertThat(gender).isEqual("M")
}
with
under the hoodwith(personDTO) {
assertThat(this.firstName).isEqual("John")
assertThat(this.lastName).isEqual("Doe")
assertThat(this.age).isEqual(37)
assertThat(this.gender).isEqual("M")
}
with
under the hoodwith(personDTO, {
assertThat(this.firstName).isEqual("John")
assertThat(this.lastName).isEqual("Doe")
assertThat(this.age).isEqual(37)
assertThat(this.gender).isEqual("M")
} )
with
under the hoodwith
→ 2 paramètres :
Le receiver
Une fonction avec comme contexte le receiver
Kotlin ❤️ AssertJ
⚠️ when
mot clé réservé en Kotlin
😭 mockito
ne sait pas mocker les classes finales
incubating extensions : mock-maker-inline
, mockito-kotlin
final
Les classes sont finales par défaut
Il faut utiliser le mot-clé open
pour explicitement autoriser l’héritage
Plusieurs solutions pour les classes que l’on veut mocker :
🤨 Ajouter le mot-clé open
😩 Définir des interfaces
Mockito
`when`(repo.findByName("John")).thenReturn(Person("John", "Doe"))
every { repo.findByName("John") } returns Person("John", "Doe")
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")
...
}
}
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")
...
}
}
🤓 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
Il faut éviter l’instanciation d’un mock avant chaque test
🐌 ça ralentit les tests !
jusqu’à 500 ms
pour le 1er mock
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")
...
}
}
Kotlin 💔 Mockito
Kotlin ❤️ Mockk
val age = 42
val json = """
{
"firstName" : "John",
"lastName" : "Doe"
"age" : $age
}
"""
data class SizeOption(val value:String)
data class ComplexSize(
val grading: SizeOption,
val specialGrading1: SizeOption? = null,
val specialGrading2: SizeOption? = null)
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")
)
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()
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")
}
}
🏭Projet actuel (2 ans et 1/2)
7 microservices en Spring Boot
☕️ 50 000 LOC en Java
Depuis 1 an
🤓 Apprentissage puis migration Kotlin
🤩 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
dataclass
(ValueObject
en DDD)
Boilerplate code 📉
persons.stream()
.map(p -> p.getName() + "-" + p.getAge())
.collect(Collectors.toList())
persons.map { "${it.name}-${it.age}" }
Pré-requis: Junit 5
, Intellij
🚀 Etapes
Configurer maven
/gradle
pour Kotlin
Créer src/test/kotlin
Ajouter la dépendance mockk
📝 Tech Blog Lectra
🎮 Kotlin Koans