Une seule assertion par test?

À la lecture de mon flux RSS, un article de blogue soulève un point souvent mal compris même parmi les vétérans des tests automatisés : est-ce que plusieurs assertion dans un test, c’est mal ? Mon point de vue n’est pas aussi tranché, mais je souhaitais revenir là-dessus.

C’est un point que j’avais déjà soulevé lors d’un précédent article sur les tests. En effet, l’auteur du livre Pragmatic Unit Testing in Java 8 with JUnit propose de n’avoir qu’une seule assertion, pendant que Robert Cecil Martin dans le livre Clean Code nuance ce propos. J’apprends par l’article que j’ai lu que l’auteur de xUnit Test Patterns: Refactoring Test Code est du même avis que le premier.

Quel est le problème d’avoir plusieurs assertions dans un test?

Un test ne devrait vérifier qu’un seul élément à la fois.
Pour vous convaincre, imaginez une application qui produit des burritos. Celui-ci aurait la capacité de produire:

  • un burrito végétarien
  • un burrito avec viande
  • un burrito avec poisson

On souhaite appliquer une réduction uniquement pour le cas d’un burrito végétarien. Comment testez-vous cela ?

Imaginons que vous ayez écrit comme test :

describe('Un burrito végétarien bénéficie une réduction', () => {
  test('un burrito végétarien possède une réduction', () => {
    // arrange
    const burrito = new Burrito(['salade', 'fromage', 'avocat', 'oeuf']);

    // act assert
    expect(burrito.estVégétarien()).toBeTruthy();
    expect(burrito.aRéduction()).toBeTruthy();
  })
});

Ici, on souhaite tester deux choses différentes :

  • qu’un burrito est végétarien
  • qu’un burrito possède une réduction

Quelles sont les raisons pour ce test d’échouer ?
Pour les deux points que je viens de soulever. Autrement dit, si on modifie le code et qu’un ou plusieurs tests échouent, je veux pouvoir identifier très simplement les raisons pour lesquelles ils ne sont plus en succès. Or, si j’ai besoin de placer un point d’arrêt dans le test pour savoir laquelle des deux assertion n’est plus valide, cela ne respecte plus le S du principe FIRST que j’ai mentionné dans un précédent article : un test en échec doit être explicite sans que je n’aie besoin de vérifier moi-même quelque chose.

Un des indices sur la mauvaise structure du test ci-dessus est dans les commentaires.
Ce n’est pas pour rien qu’on rajoute ces commentaires du AAA (ou Given When Then) : ils nous aident à voir rapidement si on est en train de faire plusieurs choses en même temps. Si vous suivez les cours de cleancoders, ou que vous avez lu son livre Clean Code, vous savez que la base de la fondation réside dans l’idée de n’avoir qu’un seul act suivi d’un seul assert.
Ici, on effectue deux acts, sous-entendu que l’on fait plusieurs choses. La solution réside dans le fait de diviser le test en deux : un qui s’occupe uniquement de produire un burrito végétarien et un autre que seul un burrito végétarien bénéficie d’une réduction

Dans ce cas, on peut réduire le champ d’action du test à la deuxième partie, en précisant dans l’arrange que le burrito que l’on soumet à notre système sous tests (SUT) est forcément un burrito végétarien.

describe('Un burrito végétarien bénéficie une réduction', () => {
  test('un burrito végétarien possède une réduction', () => {
    // arrange
    const burritoVégétarien = new Burrito(['salade', 'fromage', 'avocat', 'oeuf']);

    // act assert
    expect(burritoVégétarien.aRéduction()).toBeTruthy();
  })
});

De cette façon, on élimine l’assertion en trop, et déporte dans un autre test le cas d’un burrito végétarien. Cela aura pour conséquence de réduire la charge mentale dans la situation où plusieurs tests échouent

L’article en question

Il s’agit d’un article que j’ai pu voir défiler dans mon flux RSS.

On se situe dans une vision plus haut niveau puisque le test part de la connexion http pour ensuite entrer dans le cas métier qui nous intéresse.

J’ai mentionné plus haut qu’il peut être toléré d’avoir plusieurs assertions selon la situation. Ma condition serait de s’assurer que le champ d’action reste la même. Vous allez voir que ce n’est même pas la bonne question ici.

Si on revient sur mon exemple de burrito, plusieurs assertions ne sont pas valides puisqu’on vérifie plusieurs choses différentes. Dans l’exemple de l’article, il s’agit en vérité de la même chose : Si je supprime un restaurant, je ne dois pas pouvoir le retrouver juste après.
La première action implique la seconde. Si nous étions sur un test plus unitaire, respectant le principe FIRST, on imaginerait qu’une vérification dans le Repository en mémoire s’écrirait facilement dans l’assertion. Et s’il ne le possède pas, c’est tout naturel qu’un autre use case qui cherche à le récupérer n’y parvient pas.

En définitif, on aurait pu tester plus directement le domaine métier et avoir le même résultat que dans l’article. Dans ce cas, le test durerait moins de 10 millisecondes à s’éxecuter, pendant que dans celui du blog stack overflow, on est probablement dans la centaine de millisecondes due à la latence réseau. Appelé deux fois.

En conclusion

En bref, le principe d’avoir qu’un seul assertion est un bon indicateur pour savoir si on est dans le bon chemin ou pas. Bien sûr le test de l’article permet de vérifier une éventuelle régression. Cependant, la solution n’est pas optimisée et demandera un peu d’analyse dans le cas où le test échouerait pour une ou plusieurs raisons.

Dans les exemples de cleancoders, plusieurs assertions sont ok si on est dans le même champ d’action. Si on reprend l’exemple réseau, une idée simple serait de vérifier que la réponse est en échec, ainsi que le status code est strictement égale à 404 avec un message d’erreur. Cela nous en fait trois qui vérifient la même chose.

Comment

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *