Tester c’est douter ?

La galaxie des tests

La galaxie des tests

  • Les tests manuels

  • Les tests de charge

  • Les tests end-to-end

  • Les tests de contrat

  • Les tests d’intégrations

  • Les tests unitaires

🤯

La galaxie des tests

  • Les tests manuels

  • Les tests de charge

  • Les tests end-to-end

  • Les tests de contrat

  • Les tests d’intégrations

  • Les tests unitaires

La pyramide

test pyramid dark

Les tests unitaires

F.I.R.S.T

  • 🏃 Fast // Rapide

  • 🏝 Independent // Indépendant

  • 🔁 Repeatable // Répétable

  • ✅ Self-validating // Auto-validé

  • 📉 Thorough // Complet

🏃 Fast // Rapide

  • on veut un feedback rapide

  • on veut rester concentré

  • test lent = pas exécuté ⇒ test inutile

🏝 Independant // Indépendant

  • une seule raison d’échouer

  • pas de dépendance extérieure

    • système de fichier, bdd, random, date, …​

    • autre test

🔁 Repeatable // Répétable

  • Toujours le même résultat

  • Sinon, pas de confiance dans le test ⇒ test inutile

✅ Self-validating // Auto-validé

  • la validation est automatique

  • les conditions de validation sont incluses dans le test

📉 Thorough // Complet

  • les cas d’usage nominaux (happy path)

  • les cas aux limites (edge case)

  • les cas d’erreurs ultimes

    • base de données HS

    • HTTP 500 sur une API REST

🎁 Bonus : Simple et lisible

  • un seul niveau

  • pas de structures (if/boucles)

En pratique

On a besoin au minimum :

  • d'une syntaxe pour écrire définir les tests

  • d’instructions pour vérifier les résultats : les assertions

  • d'un outil d’exécution

Idéalement, on pourra aussi s’aider :

  • d’un outil de mesure du code testé : le coverage

  • d’instructions pour faciliter l’isolation : les mocks

Quelques outils

  • Jest, pour javascript, typescript

  • JUnit / AssertJ (assertions) / Jacoco (coverage) / Mockito (mocks) pour java

Certains langages récents intègrent nativement des outils de tests :

  • Rust

  • Elixir

Ok, et je met ça où ?

  • ça dépend de votre outil

  • généralement séparé du code de production ⇒ garder des limites claires ⇒ ne pas envoyer du code de test en production par erreur

Exemples :

  • Jest : __tests__

  • Java : src/test/java

Exécution

mvn test
junit test execution

Mesure de la couverture du code

junit test coverage

Allons-y !

Anatomie d’un test

Trois étapes :

  • Arrange

  • Act

  • Assert

Anatomie d’un test

    @Test
    void rollDice_return1() {
        /* Arrange */
        var randomGenerator = mock(RandomGenerator.class);
        when(randomGenerator.nextInt()).thenReturn(1);
        var dice = new Dice(randomGenerator);

        /* Act */
        int result = dice.roll();

        /* Assert */
        assertThat(result).isEqualTo(1);
    }

Les assertions

  • pour vérifier le résultat obtenu

  • idéalement une seule par test

Les assertions (exemple)

En JS/TS avec Jest (🌐 Documentation)

expect(age).toEqual(34)
expect(age).not.toBeLessThan(64)
expect(password).toMatch("[A-Za-z0-9]+")

En Java avec AssertJ (🌐 Documentation)

assertThat(age).isBetween(18, 100);
assertThat(wordList).containsExactlyInAnyOrder("foo", "border");
assertThat(password).matches("[a-zA-Z0-9+-$*!]+")

Le coverage

  • mesure du code exercé par les tests

  • utile pour détecter les zones non testées

  • attention à la quête impossible des 100%


Les mocks

  • permettent de donner rapidement une implémentation différente

  • on peut faire des assertions sur leur utilisation

Les mocks (exemple)

    @Test
    void rollDice_return1_withMock() {
        /* Arrange */
        var randomGenerator = mock(RandomGenerator.class);
        when(randomGenerator.nextInt()).thenReturn(1);
        var dice = new Dice(randomGenerator);

        /* Act */
        int result = dice.roll();

        /* Assert */
        assertThat(result).isEqualTo(1);
        verify(randomGenerator, times(1)).nextInt();
    }

Les doublures de test

  • permettent également de donner une implémentation différente

  • avantage : réutilisable dans d’autres tests ou dans le code de production

  • inconvénient : plus difficile de faire des assertions dessus

Les doublures de test (exemple)

    @Test
    void rollDice_return1_withTestDouble() {
        /* Arrange */
        RandomGenerator randomGenerator = () -> 1;
        var dice = new Dice(randomGenerator);

        /* Act */
        int result = dice.roll();

        /* Assert */
        assertThat(result).isEqualTo(1);
    }

Maintenir le code de test

Vous devez mettre autant de soin dans le code de test que dans le code de production
 — Mathieu Barberot
  • Le code doit rester facile à lire

  • Les code smells existent aussi dans les tests

Limiter les assertions

  • plusieurs assertions = plusieurs raisons d’échouer

  • tests fragiles

  • signe qu’on teste trop de chose d’un seul coup

⇒ limiter le nombre d’assertions à 1 ou 2 ⇒ faire plusieurs tests pour dispatcher les assertions en trop

Exemple : Avant refactoring

    @Test
    void player_can_fight() {
        // Arrange
        var kevin = new Joueur("Kevin", 5, 100, 75, 30);
        var slime = new Monstre("Slime", 5, 30, 25);

        // Act
        combatService.attack(kevin, slime);

        // Arrange
        assertThat(slime.getHp()).isZero();
        assertThat(slime.isDead()).isTrue();
        assertThat(kevin.getXp()).isZero();
        assertThat(kevin.getLevel()).isEqualTo(6);
    }

Exemple : Après refactoring

    @Test
    void monster_can_die_in_combat() {
        // Arrange
        var kevin = new Joueur("Kevin", 5, 100, 75, 30);
        var slime = new Monstre("Slime", 5, 30, 10);

        // Act
        combatService.attack(kevin, slime);

        // Arrange
        assertThat(slime.getHp()).isZero();
        assertThat(slime.isDead()).isTrue();
    }

Exemple : Après refactoring

    @Test
    void player_gain_xp_on_monster_death() {
        // Arrange
        var kevin = new Joueur("Kevin", 5, 100, 75, 30);
        var slime = new Monstre("Slime", 5, 30, 10);

        // Act
        combatService.attack(kevin, slime);

        // Arrange
        assertThat(kevin.getXp()).isEqualTo(85);
    }

Exemple : Après refactoring

    @Test
    void player_can_level_up_on_monster_death() {
        // Arrange
        var kevin = new Joueur("Kevin", 5, 100, 75, 30);
        var slime = new Monstre("Slime", 5, 30, 25);

        // Act
        combatService.attack(kevin, slime);

        // Arrange
        assertThat(kevin.getLevel()).isEqualTo(6);
    }

Eviter la duplication

  • quand les tests sont quasiment des copier/coller

Exemple : Avant refactoring

    @Test
    void formatsDateToDayMonthYear() {
        // Arrange
        // Act
        String formatted = parse("2023-01-01").format(ofPattern("dd/MM/yyyy"));
        // Assert
        assertThat(formatted).isEqualTo("01/01/2023");
    }

    @Test
    void formatsDateToMonthNameAndDay() {
        // Arrange
        // Act
        String formatted = parse("2024-12-25").format(ofPattern("MMMM d"));
        // Assert
        assertThat(formatted).isEqualTo("December 25");
    }

    @Test
    void formatsDateToIso() { /* ... */ }

Exemple : Tests paramétrés

    @ParameterizedTest(name = "formats ''{0}'' with pattern ''{1}'' in ''{2}''")
    @CsvSource({
            "2023-01-01,    dd/MM/yyyy,     01/01/2023",
            "2024-12-25,    MMMM d,         December 25",
            "2025-05-12,    yyyy.MM.dd,     2025.05.12"
    })
    void formatsDateWithGivenPattern(String input, String pattern, String expected) {
        // Arrange
        LocalDate date = parse(input);

        // Act
        String formatted = date.format(ofPattern(pattern, Locale.US));

        // Assert
        assertThat(formatted).isEqualTo(expected);
    }

Résultat

junit test parameterized

Factoriser // Arrange

  • clarifier le setup du test

  • réutiliser le code dans plusieurs tests

Exemple

    @Test
    void anItemCanBeAddedIntoACart() {
        // Arrange
        Cart cart = new Cart();

        // Act
        cart.addItem(new Item(1, "Keyboard", 100));

        // Assert
        assertThat(cart.getItems()).hasSize(1);
    }

BeforeEach / AfterEach

    private Cart cart;

    @BeforeEach
    void setUp() {
        cart = new Cart();
    }

    @Test
    void anItemCanBeAddedIntoACartUsingBeforeEach() {
        // Arrange
        // Act
        cart.addItem(new Item(1, "Keyboard", 100));

        // Assert
        assertThat(cart.getItems()).hasSize(1);
    }

BeforeAll / AfterAll

  • ⚠ cela peut rompre l’indépendance de vos tests !

  • peut être considéré comme un code smell

Limitations

  • Duplication possible entre plusieurs suites de tests

  • Séparation du code = moins de lisibilité

  • Des tests peuvent ne pas utiliser la totalité du beforeEach

Factories

  • Factorisation plus générique car réutilisable entre les suites

  • Nommage des fonctions pour la lisibilité

  • Plusieurs méthodes pour configurer le strict minimum à chaque test

Factories

    class CartFactory {
        public static Cart createEmptyCart() {
            return new Cart();
        }
    }

    @Test
    void multipleItemsCanBeAddedIntoACartUsingFactory() {
        // Arrange
        Cart emptyCart = CartFactory.createEmptyCart();

        // Act
        emptyCart.addItem(new Item(1, "Keyboard", 100));
        emptyCart.addItem(new Item(2, "Mouse", 50));

        // Assert
        assertThat(emptyCart.getItems()).hasSize(2);
    }

Aller plus loin

  • Les personas

    • pousser le concept de factories

    • incarner des types d’utilisateurs

    • requiert une coordination de l’équipe


Persona (exemple)

    class LillyFactory {
        public static Cart createCart() {
            Cart cart = new Cart();
            cart.addItem(new Item(1, "Gamer Keyboard", 200));
            cart.addItem(new Item(2, "Gamer Mouse", 100));
            return cart;
        }
    }

    @Test
    void lillyHasAnExpensiveCart() {
        // Arrange
        // Act
        Cart lillysCart = LillyFactory.createCart();

        // Assert
        assertThat(lillysCart.getTotal()).isGreaterThan(250);
    }

Les tests d’intégration

Les critères

  • Tester que les différentes parties sont bien branchées

Exemple:

  • Je tourne le volant ⇒ les roues tournent

La règle

  • Comme les tests unitaires

  • Mais avec moins de contraintes

F.I.R.S.T

  • 🏃 Fast

    • Autant que possible

  • 🏝 Independant

    • On accepte des dépendances, mais maîtrisées

      • Utiliser une base de données éphèmere

      • Utiliser des faux services tiers…​

F.I.R.S.T

  • 🔁 Repeatable

    • Toujours !

  • ✅ Self-validated

    • Toujours !

  • 🌕 Thorough

    • Le happy path et des edge cases

Avec quels outils ?

Pour une SPA :

  • Playwright

  • Cypress

  • Jest

Avec quels outils ?

Pour une API :

  • Bruno / Postman (ou équivalent)

  • Playwright

  • Hurl / Jetbrains Http Client

  • RestAssured pour Java

Tester une SPA

  1. Installer et initialiser Playwright [1]

npm init playwright@latest

Tester une SPA

  1. Installer et initialiser Playwright

  2. Écrire un test

test('Home page has the right title in page metadata', async ({ page }) => {
  // Arrange
  // Act
  await page.goto('/');

  // Assert
  await expect(page).toHaveTitle("Keyboard Factory");
});

Tester une SPA

  1. Installer et initialiser Playwright

  2. Écrire un test

  3. Exécuter une suite de test

npx playwright test

Tester une SPA

playwright report

Tester une API REST

⇒ Documenter l’API

    @OpenApi(
        path = "/rover",
        methods = {HttpMethod.POST},
        summary = "Create a rover",
        requestBody = @OpenApiRequestBody(
            content = {@OpenApiContent(from = RoverCreationDto.class)},
            required = true
        ),
        responses = {@OpenApiResponse(status = "201")}
    )
    public static void create(Context ctx) {
       /* ... */
    }

Tester une API REST

⇒ Documenter les structures de données

public record CreateActionDto(
    @OpenApiRequired
    @OpenApiDescription("Le nom du rover")
    String rover,
    @OpenApiRequired
    @OpenApiDescription("L'action à effectuer : forward, backward, turn_left, turn_right")
    String action
) {}

Tester une API REST

⇒ Initialiser un client REST

rest client 01 create collection

Tester une API REST

⇒ Importer via la documentation OpenAPI

rest client 02 import from openapi

Tester une API REST

rest client 03 finalize import

Tester une API REST

rest client 04 import successful

Tester une API REST

⇒ Configurer l’environnement

rest client 05 configure environment

Tester une API REST

rest client 06 environment

Tester une API REST

⇒ Exécuter une requête

rest client 07 execute request

Tester une API REST

⇒ Ajouter des assertions

rest client 08 add assertions

Tester une API REST

⇒ Ou carrément écrire des tests en JS

rest client 09 add tests

Merci de votre attention