Automatisation · 5 min de lecture ·

Architecturer un framework d’automatisation qui survit à trois ans

Enthousiasme la première année, essoufflement la deuxième, abandon la troisième. Cinq décisions de structure qui ne coûtent rien au départ et déterminent si quelqu’un lira encore le résultat dans trois ans.

Presque toutes les suites de tests automatisés que nous auditons ont connu le même cycle : enthousiasme la première année, essoufflement la deuxième, abandon partiel la troisième. Le code est toujours là, il tourne encore en intégration, mais plus personne ne regarde le résultat.

Ce n’est presque jamais un problème d’outil. C’est un problème de décisions de structure, prises — ou non prises — au tout début.

En voici cinq. Elles ne coûtent rien à la création et se paient très cher après.

Décision 1 — Séparer l’intention du mécanisme

C’est la décision fondatrice. Tout le reste en découle.

Un test doit exprimer ce qu’on vérifie. Il ne doit rien savoir de comment on y accède.

// Ce qui arrive quand la décision n'est pas prise.
@Test
void testCheckout() {
    driver.findElement(By.xpath("//div[@class='cart']//button[2]")).click();
    Thread.sleep(2000);
    driver.findElement(By.id("cc-num")).sendKeys("4242424242424242");
    driver.findElement(By.cssSelector(".btn-primary")).click();
    assertTrue(driver.getPageSource().contains("Merci"));
}

Ce test est illisible, il casse au premier changement de mise en page, et il ne dit pas ce qu’il vérifie. Le nom testCheckout est la seule information métier du bloc, et elle est fausse : le test vérifie en réalité qu’un mot apparaît quelque part dans la page.

// La même intention, exprimée.
@Test
void shouldConfirmOrderWhenPaymentSucceeds() {
    cartPage.proceedToCheckout();
    checkoutPage.payWith(VALID_CARD);

    assertThat(confirmationPage.orderNumber()).isNotBlank();
    assertThat(confirmationPage.status()).isEqualTo(CONFIRMED);
}

La règle opérationnelle, qui se vérifie par un grep : aucun sélecteur ne doit apparaître dans un fichier de test. Si By. ou page.locator( apparaît hors des objets de page, l’abstraction a fui, et elle fuira de plus en plus.

Décision 2 — Les données appartiennent au test qui les utilise

C’est la première cause d’instabilité, et de loin.

Un jeu de données partagé entre tests crée une dépendance invisible entre eux. Le test A modifie le client 42, le test B le lit et échoue — mais seulement quand ils s’exécutent dans cet ordre, donc seulement en parallèle, donc seulement en intégration, donc jamais quand vous déboguez en local.

// Chaque test crée ce dont il a besoin, et le détruit.
@Test
void shouldBlockOrderWhenCreditLimitExceeded() {
    Customer customer = customers.create(withCreditLimit(500));

    Order order = orders.placeFor(customer, amount(800));

    assertThat(order.status()).isEqualTo(REJECTED);
}

Trois propriétés à viser, dans cet ordre :

  • isolation : aucun test ne dépend de l’état laissé par un autre ;
  • indépendance à l’ordre : la suite passe si on la mélange. C’est un test à faire une fois par trimestre, et il révèle toujours quelque chose ;
  • nettoyage : le test défait ce qu’il a fait, même s’il échoue.

Le coût de construction est réel : il faut des fabriques de données et des points d’entrée pour créer l’état. C’est un investissement, et c’est celui qui rapporte le plus.

Décision 3 — Attendre une condition, jamais une durée

Thread.sleep(3000) n’est pas une attente. C’est un pari sur la vitesse de la machine.

Ce pari est perdant deux fois : trop court sur une machine d’intégration chargée, et trop long sur un poste rapide — où il fait perdre trois secondes par occurrence, multipliées par le nombre de tests.

// Un pari.
Thread.sleep(3000);
driver.findElement(By.id("result")).click();

// Une condition. Le test avance dès que c'est possible,
// et échoue avec un message clair si ça n'arrive jamais.
wait.until(ExpectedConditions.elementToBeClickable(By.id("result"))).click();

Corollaire souvent oublié : il faut aussi savoir attendre la fin de quelque chose — une requête réseau, une animation. Les frameworks modernes attendent automatiquement une bonne part de ces conditions ; c’est leur principal apport, bien avant leur syntaxe.

Décision 4 — Le bon niveau, décidé explicitement

La question « faut-il automatiser ce scénario ? » est mal posée. La bonne question est : « à quel niveau ce scénario est-il le moins cher à vérifier ? »

Un exemple concret, sur un calcul de remise :

Niveau Durée Ce qu’il vérifie Ce qu’il ne dit pas
Unitaire ~5 ms Le calcul est correct Que l’application l’utilise
API ~80 ms Le service applique la règle Que l’interface l’affiche
Interface ~8 s L’utilisateur voit le bon prix Rien de plus que les deux précédents

Vérifier les quinze combinaisons de la règle de remise en interface coûte deux minutes d’exécution et une forte probabilité d’instabilité. Les vérifier en unitaire coûte 75 millisecondes.

La règle : une seule vérification de bout en bout confirme que le circuit complet fonctionne. Toutes les variantes descendent au niveau le plus bas qui puisse les observer.

Les équipes qui font ce déplacement divisent typiquement par cinq la durée de leur campagne. C’est le levier le plus rentable de tout l’exercice, et il ne demande aucun outil nouveau.

Décision 5 — Décider tout de suite ce qu’on fera d’un test instable

C’est la décision la moins technique, et celle qui détermine si la suite survit.

Un test instable est un test qui échoue parfois sans que le produit ait changé. Il y en aura. La seule question est ce que vous en faites.

Ce qui tue une suite : ajouter un mécanisme de réexécution automatique. C’est tentant, ça fait passer la chaîne au vert, et ça détruit la valeur de l’ensemble. Parce qu’un test qui passe à la troisième tentative ne vous dit plus rien — et que l’équipe apprend à relancer plutôt qu’à investiguer. À partir de ce moment, le rouge ne signifie plus rien.

Ce qui la sauve : une règle écrite, appliquée sans exception.

  • un test instable est identifié, tracé, et son taux d’échec suivi ;
  • il est corrigé ou retiré de la campagne bloquante sous 48 heures ;
  • un test retiré est soit réparé sous deux semaines, soit supprimé.

Supprimer un test fait mal. Garder un test auquel personne ne croit fait bien plus de dégâts : il coûte du temps d’exécution, il occupe l’attention, et il dégrade la confiance dans tous les autres.

Le signe qui ne trompe pas

Il existe un indicateur unique de la santé d’une suite automatisée, et ce n’est ni la couverture, ni le nombre de tests, ni le taux de réussite.

C’est la réponse à cette question, posée à un développeur de l’équipe :

Quand la chaîne passe au rouge, qu’est-ce que tu fais ?

Si la réponse est « je regarde ce qui a cassé », la suite est vivante.

Si la réponse est « je relance », elle est morte — et le code qui la constitue n’est plus qu’un coût d’exécution.

Ces cinq décisions n’ont rien de spectaculaire. Elles ne coûtent presque rien le premier jour. Mais elles déterminent si, dans trois ans, quelqu’un lira encore le résultat.

Omar OUJAA

Fondateur · Full Stack QA · DevOps

Ingénieur, certifié ISTQB Test Manager, auteur aux Éditions ENI. Fondateur de STRATEST en 2018.

En savoir plus