Si le mot test vous fait fuir et que le mot DSL n’évoque pour vous que des mauvais souvenirs, restez quand même ! Dans ce billet, je vais tâcher de vous montrer comment, grâce à Kotlin, on peut utiliser un DSL pour structurer et rendre plus lisible vos tests… et sans les alourdir !
Petit rappel
Ah le test automatisé… Indispensable élément fondateur de notre pyramide de tests, il est aujourd’hui pleinement outillé de patterns, frameworks et autres sucreries.
Dès lors, la plupart du temps nous respectons scrupuleusement la structuration Given, When, Then (ou AAA pour Arrange Act Assert) afin de disposer de jolis bouts de code lisibles.
Egalement, depuis un bon nombre d’années, le principe d’inversion de contrôle (IoC), mis en oeuvre via l’injection de dépendances, nous permet de mocker les comportements des dépendances au sein du code testé afin de contrôler l’attendu de notre test (une grosse partie du Given & du Then de notre pattern de test).
En pratique, ça donne ce genre de code :
fun `should get the Giant octopus` () {
// Given
val giantOctopus = Octopus (
type = OctopusType.GIANT,
name = "Garganctopus"
)
whenever(octopusRepository.findByType(any<OctopusType>()).thenReturn(giantOctopus)
// When
val foundOctopus = octopusService.findTheGiantOctopus()
// Then
assertThat(foundOctopus).isEqualTo(giantOctopus)
verify(octopusRepository).findByType(OctopusType.GIANT)
}
Gestion de vos données de tests
“On a pas le même package, mais on a la même passion”
Le test automatisé, c’est du code. Du code qui doit respecter les principes de POO comme le reste…. et je vais m’intéresser à un principe qui est largement oublié dans le test unitaire c’est DRY (Don’t Repeat Yourself).
En effet, quand votre projet grossit, le nombre de tests augmente, et comme un test unitaire doit tester UNE chose et UNE seule, il est courant d’avoir besoin des mêmes données de tests au sein de différents tests unitaires.
Des factories de données de tests
“Extract Method”
La première solution pour pallier ce problème est le bien utile “Extract Method”.
Disponible dans tous vos IDE favoris, il permet de factoriser facilement ce genre de code mais l’on voit rapidement ses limites…
private fun createGiantOctopus = Octopus (
type = OctopusType.GIANT,
name = "Garganctopus"
)
En effet, si l’on peut considérer acceptable d’avoir des méthodes de ce type au sein de nos classes de tests, encore une fois, respectueux des bonnes pratiques de POO, on ne peut utiliser ces méthodes à l’extérieur de cette classe. Ainsi, le plus évident sera donc de déporter ce code dans une classe dédiée à la création des objets de tests (i.e. une factory pour ceux qui n’ont pas suivi)
fun `should get the Giant octopus` () {
// Given
val giantOctopus = octopusMockFactory.createGiantOctopus()
whenever(octopusRepository.findByType(any<OctopusType>()).thenReturn(giantOctopus)
// When
val foundOctopus = octopusService.findTheGiantOctopus()
// Then
assertThat(foundOctopus).isEqualTo(giantOctopus)
verify(octopusRepository).findByType(OctopusType.GIANT)
}
Du DSL pour amener du fonctionnel dans notre code
La plupart du temps, ces factories répondent à nos attentes. Cependant, essayons d’aller plus loin….
Pourquoi ne pas faire quelque chose de plus fonctionnel (sisi pourquoi pas :) ) ? C’est en cela que Kotlin va nous aider.
Le but est simple, transformer notre bonne vieille factory en pseudo-DSL fonctionnel.
Et voici comment faire :
fun `given a giant octopus` (block: OctopusContextWrapper.() -> Unit) {
block.invoke(OctopusContextWrapper(
octopus = Octopus (
type = OctopusType.GIANT,
name = "Garganctopus"
)
))
}
class OctopusContextWrapper(val octopus: Octopus)
Pour ceux qui ne sont pas familier avec Kotlin, ce bout code mérite quelques explications.
- C’est donc une fonction
- Qui a pour nom
given a giant octopus
, oui oui, une vraie phrase - Qui prend en paramètre une fonction ou lambda block
- Qui ne prend aucun paramètre et ne retourne rien (Unit en Kotlin)
- Ayant pour contexte d’exécution un OctopusContextWrapper, et donc ayant accès aux attributs de cet objet (ou function type with receiver en anglais)
- Qui invoque la lambda block en passant le contexte correspondant à notre méthode
- Qui a pour nom
- Et en bonus, grâce à Kotlin, notre méthode est définie comme globale et donc peut être appelée sans préfixe de classe
Et voici son utilisation au sein du code.
fun `should get the Giant octopus` () {
`given a giant octopus` {
whenever(octopusRepository.findByType(any<OctopusType>()).thenReturn(this.octopus)
// When
val foundOctopus = octopusService.findTheGiantOctopus()
// Then
assertThat(foundOctopus).isEqualTo(this.octopus)
verify(octopusRepository).findByType(OctopusType.GIANT)
}
}
Notre fonction est ici bien appelée avec une lambda en paramètre (définie par des accolades) et on dispose dans this de l’attribut de notre fameux OctopusContextWrapper.
Note : en Kotlin, si le dernier paramètre est une lambda, les parenthèses peuvent être omise.
Au sein de ce bloc de code, nous avons à disposition les attributs de nos wrappers, ici octopus.
Il est évidemment possible de les enchaîner, comme ici :
fun `the Giant Octopus should win against the mega shark` () {
`given a giant octopus` {
`given a mega shark` {
// Fight this.octopus & this.shark !
}
}
}
Le petit bonus c’est aussi que vous allez disposer de l’autocomplétion sur ce genre de fonction afin que, lorsque vous débutez votre test unitaire, vous choisissiez votre contexte de test :
Et voilà comment Kotlin et la conception en mode DSL nous offre une lecture très claire et fonctionnelle du test et surtout du contexte de données qu’il utilise.
Aller plus loin
Après la lecture de cet article, que vous soyez conquis ou non, j’espère vous avoir montré comment utiliser Kotlin pour construire et structurer simplement des tests. La construction de DSL a énormément d’applications et en voici une… maintenant à vous de créer la vôtre !