Contactez-nous

Introduction aux tests unitaires (JUnit/kotlin.test)

Découvrez l'importance des tests unitaires en Kotlin. Apprenez les bases de l'écriture de tests avec JUnit 5 et les assertions de la bibliothèque `kotlin.test`.

Introduction : Garantir la qualité et la robustesse du code

Ecrire du code fonctionnel est une chose, mais s'assurer qu'il fonctionne correctement dans toutes les conditions, qu'il gère les cas limites et qu'il continue de fonctionner après des modifications futures en est une autre. C'est là qu'interviennent les tests automatisés, et en particulier les tests unitaires.

Un test unitaire est un petit morceau de code qui vérifie le comportement d'une unité isolée de votre code source – typiquement une fonction, une méthode, ou parfois une classe entière – en lui fournissant des entrées spécifiques et en validant que les sorties ou les effets de bord produits sont conformes aux attentes. L'objectif est de vérifier chaque "unité" de manière indépendante pour s'assurer qu'elle remplit correctement son contrat.

L'écriture de tests unitaires est une pratique fondamentale du développement logiciel professionnel. Elle offre de nombreux avantages :

  • Détection précoce des bugs : Attrape les erreurs au plus tôt dans le cycle de développement, quand elles sont moins coûteuses à corriger.
  • Confiance dans le refactoring : Permet de modifier et d'améliorer le code existant avec l'assurance que les fonctionnalités clés ne sont pas cassées (régression).
  • Documentation vivante : Les tests servent d'exemples concrets sur la manière d'utiliser le code testé.
  • Meilleure conception : Ecrire du code testable encourage souvent une conception plus modulaire et découplée.
  • Facilitation de l'intégration continue : Les tests peuvent être exécutés automatiquement à chaque modification du code.

Kotlin, s'intégrant parfaitement à l'écosystème JVM, peut utiliser les frameworks de test Java populaires comme JUnit, tout en offrant également sa propre bibliothèque `kotlin.test` pour une expérience plus idiomatique.

Mise en place : Dépendances et structure

Pour écrire des tests unitaires en Kotlin, vous aurez généralement besoin d'ajouter des dépendances de test à votre projet. Si vous utilisez un outil de build comme Gradle ou Maven, c'est là que vous les déclarerez.

Les choix courants sont :

  • JUnit 5 (Jupiter) : Le standard de facto pour les tests unitaires sur la JVM. Très mature et riche en fonctionnalités.
  • `kotlin.test` : Une bibliothèque fournie par JetBrains qui offre une abstraction par-dessus différents frameworks de test (JUnit 4, JUnit 5, TestNG, JS, Native). Elle fournit des fonctions d'assertion plus idiomatiques pour Kotlin.

Souvent, on utilise `kotlin.test` en conjonction avec JUnit 5 comme moteur d'exécution sous-jacent. Voici un exemple de configuration Gradle (Kotlin DSL) :

// build.gradle.kts (extrait)

dependencies {
    // Dépendance principale de kotlin.test
    testImplementation(kotlin.test.junit5) // Utilise JUnit 5 comme backend

    // Dépendances JUnit 5 (nécessaires pour l'exécution)
    testImplementation(platform("org.junit:junit-bom:5.9.1"))
    testImplementation("org.junit.jupiter:junit-jupiter")

    // Pour faire tourner les tests
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

tasks.test {
    useJUnitPlatform() // Indique à Gradle d'utiliser JUnit Platform
}

Les fichiers de test sont généralement placés dans un répertoire source séparé, conventionnellement `src/test/kotlin` (parallèle à `src/main/kotlin`). La structure des packages dans le répertoire de test devrait idéalement refléter celle du code principal pour une meilleure organisation.

Ecrire un test simple avec JUnit 5 et `kotlin.test`

Un test unitaire est typiquement une méthode à l'intérieur d'une classe de test. Avec JUnit 5, on annote la classe de test et les méthodes de test avec des annotations spécifiques.

Supposons que nous ayons une fonction simple à tester :

// Dans src/main/kotlin/com/example/math/Calculator.kt
package com.example.math

class Calculator {
    fun add(a: Int, b: Int): Int {
        return a + b
    }

    fun divide(a: Int, b: Int): Double {
        require(b != 0) { "Division by zero is not allowed." }
        return a.toDouble() / b
    }
}

Nous pouvons écrire une classe de test correspondante :

// Dans src/test/kotlin/com/example/math/CalculatorTest.kt
package com.example.math

import kotlin.test.assertEquals // Assertion de kotlin.test
import kotlin.test.assertFailsWith // Assertion pour les exceptions
import org.junit.jupiter.api.Test // Annotation de test JUnit 5
import org.junit.jupiter.api.DisplayName // Annotation optionnelle pour un nom lisible

internal class CalculatorTest { // La classe de test

    private val calculator = Calculator() // Crée l'instance à tester

    @Test // Marque cette fonction comme un cas de test
    @DisplayName("Test d'addition simple")
    fun `test simple addition`() { // Nom de fonction descriptif (peut utiliser des backticks)
        // Arrange (Préparation) - déjà fait avec calculator
        val a = 5
        val b = 3

        // Act (Action) - appelle la méthode à tester
        val result = calculator.add(a, b)

        // Assert (Vérification) - vérifie que le résultat est correct
        val expected = 8
        assertEquals(expected, result, "L'addition 5 + 3 devrait être 8")
    }

    @Test
    fun `test division standard`() {
        val result = calculator.divide(10, 2)
        assertEquals(5.0, result, 0.001) // On peut ajouter une tolérance pour les doubles
    }

    @Test
    fun `test division by zero should throw exception`() {
        // Vérifie qu'une exception spécifique est levée
        val exception = assertFailsWith(
            message = "Division par zéro attendue" // Message optionnel en cas d'échec du test
        ) {
            calculator.divide(10, 0) // Code qui doit lever l'exception
        }
        // On peut aussi vérifier le message de l'exception si nécessaire
        assertEquals("Division by zero is not allowed.", exception.message)
    }
}
Structure d'un test (AAA) :
  • Arrange : Mettre en place les préconditions du test (créer des objets, initialiser des données).
  • Act : Exécuter l'unité de code que l'on souhaite tester.
  • Assert : Vérifier que le résultat obtenu (ou l'état final) est conforme à ce qui était attendu. C'est là qu'on utilise les fonctions d'assertion (`assertEquals`, `assertTrue`, `assertNotNull`, `assertFailsWith`, etc.).

Les annotations (`@Test`, `@DisplayName`) proviennent de JUnit 5. Les fonctions d'assertion (`assertEquals`, `assertFailsWith`) proviennent de `kotlin.test` et offrent une syntaxe agréable en Kotlin.

Assertions courantes avec `kotlin.test`

La bibliothèque `kotlin.test` fournit un ensemble riche d'assertions idiomatiques :

  • `assertEquals(expected, actual, message?)` : Vérifie l'égalité.
  • `assertNotEquals(illegal, actual, message?)` : Vérifie l'inégalité.
  • `assertSame(expected, actual, message?)` : Vérifie que deux références pointent vers le même objet (égalité référentielle).
  • `assertNotSame(illegal, actual, message?)` : Vérifie l'inégalité référentielle.
  • `assertTrue(actual, message?)` / `assertTrue { condition }` : Vérifie qu'une valeur ou condition est vraie.
  • `assertFalse(actual, message?)` / `assertFalse { condition }` : Vérifie qu'une valeur ou condition est fausse.
  • `assertNotNull(actual, message?)` / `assertNotNull(actual) { block }` : Vérifie qu'une valeur n'est pas nulle (et permet d'utiliser la valeur non-nulle dans le bloc optionnel).
  • `assertNull(actual, message?)` : Vérifie qu'une valeur est nulle.
  • `assertFails(message?, block)` : Vérifie que le bloc de code lève n'importe quelle exception.
  • `assertFailsWith(message?, block)` : Vérifie que le bloc de code lève une exception du type spécifique `T` (ou un sous-type). Retourne l'exception levée.
  • `fail(message)` : Fait échouer le test immédiatement.

Ces assertions rendent les vérifications claires et concises dans vos tests.

Exécuter les tests

Vous pouvez exécuter vos tests unitaires de plusieurs manières :

  • Depuis l'IDE (IntelliJ IDEA / Android Studio) : L'IDE détecte les classes et méthodes de test et affiche des icônes (souvent des flèches vertes) dans la marge pour lancer des tests individuels, tous les tests d'une classe, ou tous les tests d'un module. Les résultats sont affichés dans une fenêtre dédiée.
  • Via l'outil de build (Gradle/Maven) : En exécutant une tâche spécifique depuis la ligne de commande (par exemple, `./gradlew test` ou `mvn test`). Cela exécute tous les tests du projet et génère souvent un rapport HTML détaillé. C'est la méthode utilisée par les systèmes d'intégration continue.

Conclusion : Une pratique indispensable

Les tests unitaires sont bien plus qu'une simple vérification ; ils sont une partie intégrante du processus de développement qui améliore la qualité, la maintenabilité et la fiabilité de votre code Kotlin. L'écosystème Kotlin, grâce à son interopérabilité avec Java et à sa propre bibliothèque `kotlin.test`, offre des outils puissants et agréables pour écrire et exécuter ces tests.

Même si cette section n'est qu'une introduction, elle vous donne les bases pour commencer à intégrer les tests unitaires dans vos projets. Investir du temps dans l'écriture de bons tests est un investissement qui porte ses fruits à long terme en réduisant les bugs et en augmentant votre confiance dans votre base de code.