Devrait-il être «Arranger-Assert-Act-Assert»?

94

En ce qui concerne le modèle de test classique de Arrange-Act-Assert , je me retrouve souvent à ajouter une contre-affirmation qui précède Act. De cette façon, je sais que l'assertion passagère est en fait le résultat de l'action.

Je pense que c'est analogue au rouge en rouge-vert-refactor, où ce n'est que si j'ai vu la barre rouge au cours de mes tests que je sais que la barre verte signifie que j'ai écrit du code qui fait une différence. Si j'écris un test de réussite, alors n'importe quel code le satisfera; de même, en ce qui concerne Arrange-Assert-Act-Assert, si ma première affirmation échoue, je sais que n'importe quelle loi aurait adopté la déclaration finale - de sorte qu'elle ne vérifiait en fait rien au sujet de la Loi.

Vos tests suivent-ils ce modèle? Pourquoi ou pourquoi pas?

Mise à jour Clarification: l'assertion initiale est essentiellement l'opposé de l'assertion finale. Ce n'est pas une affirmation qu'Arrange a fonctionné; c'est une affirmation que Act n'a pas encore fonctionné.

Carl Manaster
la source

Réponses:

121

Ce n'est pas la chose la plus courante à faire, mais assez courante pour avoir son propre nom. Cette technique s'appelle l' assertion de garde . Vous pouvez en trouver une description détaillée à la page 490 dans l'excellent livre xUnit Test Patterns de Gerard Meszaros (fortement recommandé).

Normalement, je n'utilise pas ce modèle moi-même, car je trouve plus correct d'écrire un test spécifique qui valide toute condition préalable que je ressens le besoin de garantir. Un tel test devrait toujours échouer si la condition préalable échoue, et cela signifie que je n'en ai pas besoin intégré dans tous les autres tests. Cela donne une meilleure isolation des préoccupations, puisqu'un cas de test ne vérifie qu'une seule chose.

Il peut y avoir de nombreuses conditions préalables qui doivent être satisfaites pour un cas de test donné, vous pouvez donc avoir besoin de plusieurs assertions de garde. Au lieu de les répéter dans tous les tests, avoir un (et un seul) test pour chaque condition préalable permet de garder votre code de test plus durable, car vous aurez moins de répétition de cette façon.

Mark Seemann
la source
+1, très bonne réponse. La dernière partie est particulièrement importante, car elle montre que vous pouvez garder les choses comme un test unitaire séparé.
murrekatt
3
Je l'ai généralement fait de cette façon aussi, mais il y a un problème avec un test séparé pour garantir les conditions préalables (en particulier avec une grande base de code avec des exigences changeantes) - le test de condition préalable sera modifié au fil du temps et se désynchronise avec le `` principal '' test qui présuppose ces conditions préalables. Les conditions préalables peuvent donc être toutes bonnes et vertes, mais ces conditions ne sont pas satisfaites dans le test principal, qui est désormais toujours vert et correct. Mais si les conditions préalables étaient dans le test principal, elles auraient échoué. Avez-vous rencontré ce problème et avez-vous trouvé une solution intéressante?
nchaud
2
Si vous modifiez beaucoup vos tests, vous pouvez avoir d'autres problèmes , car cela aura tendance à rendre vos tests moins fiables. Même face à l'évolution des exigences, envisagez de concevoir du code de manière à ajouter uniquement .
Mark Seemann
@MarkSeemann Vous avez raison, que nous devons minimiser la répétition, mais de l'autre côté, il peut y avoir beaucoup de choses, qui peuvent avoir un impact sur l'Arrange pour le test spécifique, bien que le test d'Arrange lui-même passe. Par exemple, le nettoyage pour le test Arrangement ou après un autre test était défectueux et Arrange ne serait pas le même que dans le test Arrange.
Rekshino
32

Il peut également être spécifié comme Arrange- Assume -Act -Assert.

Il y a une poignée technique pour cela dans NUnit, comme dans l'exemple ici: http://nunit.org/index.php?p=theory&r=2.5.7

Ole Lynge
la source
1
Agréable! J'aime un quatrième - et différent - et précis - "A". Merci!
Carl Manaster
+1, @Ole! J'aime celle-ci aussi, pour certains cas particuliers! Je vais essayer!
John Tobler
8

Voici un exemple.

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
    range.encompass(7);
    assertTrue(range.includes(7));
}

Il se peut que j'écrive Range.includes()simplement pour retourner vrai. Je ne l'ai pas fait, mais je peux imaginer que je pourrais avoir. Ou j'aurais pu mal l'écrire de plusieurs autres manières. J'espérerais et je m'attendrais à ce qu'avec TDD, j'ai vraiment réussi - cela includes()fonctionne juste - mais peut-être que je ne l'ai pas fait. La première assertion est donc une vérification de bon sens, pour s'assurer que la seconde assertion est vraiment significative.

Lire par lui-même, assertTrue(range.includes(7));c'est dire: "affirmer que la plage modifiée comprend 7". Lire dans le contexte de la première assertion, il dit: "affirmer qu'invoquer encompass () le fait inclure 7. Et comme encompass est l'unité que nous testons, je pense que cela a une (petite) valeur.

J'accepte ma propre réponse; beaucoup d'autres ont mal interprété ma question comme étant de tester la configuration. Je pense que c'est légèrement différent.

Carl Manaster
la source
Merci d'être revenu avec un exemple, Carl. Eh bien, dans la partie rouge du cycle TDD, jusqu'à ce que l'encompass () fasse vraiment quelque chose; la première assertion est inutile, ce n'est qu'une duplication de la seconde. Au vert, cela commence à être utile. Cela prend du sens lors du refactoring. Cela pourrait être bien d'avoir un framework UT qui le fasse automatiquement.
philant
Supposons que vous TDD cette classe Range, n'y aura-t-il pas un autre test échouant pour tester le Range ctor, quand vous le casserez?
philant
1
@philippe: Je ne suis pas sûr de comprendre la question. Le constructeur Range et includes () ont leurs propres tests unitaires. Pourriez-vous élaborer, s'il vous plaît?
Carl Manaster
Pour que la première assertFalse (range.includes (7)) échoue, vous devez avoir un défaut dans le constructeur de plage. Je voulais donc demander si les tests du constructeur Range ne s'interrompent pas en même temps que cette assertion. Et qu'en est-il de l'affirmation après la loi sur une autre valeur: par exemple assertFalse (range.includes (6))?
philant du
1
La construction de plage, à mon avis, vient avant des fonctions comme includes (). Donc, même si je suis d'accord, seul un constructeur défectueux (ou un includes () défectueux) ferait échouer cette première assertion, le test du constructeur n'inclurait pas un appel à includes (). Oui, toutes les fonctions jusqu'à la première assertion sont déjà testées. Mais cette affirmation négative initiale communique quelque chose et, à mon avis, quelque chose d'utile. Même si chaque assertion de ce type passe lors de son écriture initiale.
Carl Manaster
7

Un Arrange-Assert-Act-Asserttest peut toujours être refactorisé en deux tests:

1. Arrange-Assert

et

2. Arrange-Act-Assert

Le premier test n'affirmera que sur ce qui a été mis en place dans la phase d'arrangement, et le second test ne s'appliquera que pour ce qui s'est passé dans la phase d'acte.

Cela a l'avantage de donner un retour plus précis sur si c'est la phase Arranger ou Act qui a échoué, alors que dans l'original, Arrange-Assert-Act-Assertelles sont confondues et vous devriez creuser plus profondément et examiner exactement quelle assertion a échoué et pourquoi elle a échoué afin de savoir si c'était l'arrangement ou la loi qui a échoué.

Il répond également mieux à l'intention des tests unitaires, car vous séparez votre test en unités indépendantes plus petites.

Enfin, gardez à l'esprit que chaque fois que vous voyez des sections d'arrangement similaires dans différents tests, vous devriez essayer de les extraire dans des méthodes d'aide partagées, afin que vos tests soient plus DRY. et plus maintenables à l'avenir.

Sammi
la source
3

Je fais maintenant cela. AAAA d'un type différent

Arrange - setup
Act - what is being tested
Assemble - what is optionally needed to perform the assert
Assert - the actual assertions

Exemple de test de mise à jour:

Arrange: 
    New object as NewObject
    Set properties of NewObject
    Save the NewObject
    Read the object as ReadObject

Act: 
    Change the ReadObject
    Save the ReadObject

Assemble: 
    Read the object as ReadUpdated

Assert: 
    Compare ReadUpdated with ReadObject properties

La raison en est que l'ACT ne contient pas la lecture de la ReadUpdated parce qu'elle ne fait pas partie de l'acte. L'acte ne fait que changer et sauver. Donc vraiment, ARRANGE ReadUpdated pour l'assertion, j'appelle ASSEMBLE pour l'assertion. Ceci afin d'éviter de confondre la section ARRANGE

ASSERT ne doit contenir que des assertions. Cela laisse ASSEMBLE entre ACT et ASSERT qui met en place l'assert.

Enfin, si vous échouez dans l'Arrangement, vos tests ne sont pas corrects car vous devriez avoir d'autres tests pour éviter / trouver ces bugs triviaux . Parce que pour le scénario que je présente, il devrait déjà y avoir d'autres tests qui testent READ et CREATE. Si vous créez une "Assertion de garde", vous risquez de rompre DRY et de créer une maintenance.

Valamas
la source
1

Lancer une assertion de «contrôle de cohérence» pour vérifier l'état avant d'effectuer l'action que vous testez est une vieille technique. Je les écris généralement comme des échafaudages de test pour me prouver que le test fait ce que j'attends, et je les supprime plus tard pour éviter d'encombrer les tests avec des échafaudages de test. Parfois, laisser l'échafaudage aide le test à servir de récit.

Dave W. Smith
la source
1

J'ai déjà lu cette technique - peut-être de votre part - mais je ne l'utilise pas; surtout parce que je suis habitué à la forme triple A pour mes tests unitaires.

Maintenant, je deviens curieux et j'ai quelques questions: comment rédigez-vous votre test, faites-vous échouer cette assertion, en suivant un cycle de refactorisation rouge-vert-rouge-vert, ou l'ajoutez-vous après?

Échouez-vous parfois, peut-être après avoir refactorisé le code? Qu'est-ce que cela vous dit? Peut-être pourriez-vous partager un exemple où cela a aidé. Merci.

philant
la source
En général, je ne force pas l'assertion initiale à échouer - après tout, elle ne devrait pas échouer, comme une assertion TDD le devrait, avant que sa méthode ne soit écrite. Je ne l' écris, quand je l' écris, avant , juste dans le cours normal de l' écriture du test, et non après. Honnêtement, je ne me souviens pas qu'il ait échoué - peut-être que cela suggère que c'est une perte de temps. Je vais essayer de trouver un exemple, mais je n'en ai pas en tête pour le moment. Merci pour les questions; ils sont utiles.
Carl Manaster
1

J'ai déjà fait cela lors d'une enquête sur un test qui a échoué.

Après un grattage considérable de la tête, j'ai déterminé que la cause était que les méthodes appelées pendant «Arranger» ne fonctionnaient pas correctement. L'échec du test était trompeur. J'ai ajouté un Assert après l'arrangement. Cela a fait échouer le test dans un endroit qui a mis en évidence le problème réel.

Je pense qu'il y a aussi une odeur de code ici si la partie Arranger du test est trop longue et compliquée.

WW.
la source
Un point mineur: je considérerais l'Arrangement trop compliqué plus comme une odeur de design qu'une odeur de code - parfois la conception est telle que seule une Arrangement compliquée vous permettra de tester l'unité. Je le mentionne parce que cette situation veut une solution plus profonde qu'une simple odeur de code.
Carl Manaster
1

En général, j'aime beaucoup "Organiser, Agir, Affirmer" et l'utiliser comme norme personnelle. La seule chose qu'il ne me rappelle pas de faire, cependant, est de désorganiser ce que j'ai arrangé lorsque les affirmations sont faites. Dans la plupart des cas, cela ne cause pas beaucoup de gêne, car la plupart des choses disparaissent automatiquement via le garbage collection, etc. Si vous avez établi des connexions à des ressources externes, cependant, vous voudrez probablement fermer ces connexions lorsque vous avez terminé avec vos affirmations ou vous avez beaucoup un serveur ou une ressource coûteuse quelque part qui détient des connexions ou des ressources vitales qu'il devrait être en mesure de donner à quelqu'un d'autre. Ceci est particulièrement important si vous partie de ces développeurs qui n'utilisent pas TearDown ou TestFixtureTearDownà nettoyer après un ou plusieurs tests. Bien entendu, "Organiser, Agir, Assertir" n'est pas responsable de mon échec à fermer ce que j'ouvre; Je ne mentionne ce "gotcha" que parce que je n'ai pas encore trouvé de bon synonyme de "A-word" pour "disposer" à recommander! Aucune suggestion?

John Tobler
la source
1
@carlmanaster, vous êtes en fait assez proche pour moi! Je colle cela dans mon prochain TestFixture pour l'essayer pour la taille. C'est comme ce petit rappel de faire ce que votre mère aurait dû vous apprendre: "Si vous l'ouvrez, fermez-le! Si vous le gâchez, nettoyez-le!" Peut-être que quelqu'un d'autre peut l'améliorer, mais au moins cela commence par un "a!" Merci pour votre suggestion!
John Tobler
1
@carlmanaster, j'ai essayé "Annul". C'est mieux que «démonter», et ça marche en quelque sorte, mais je suis toujours à la recherche d'un autre mot «A» qui me colle à la tête aussi parfaitement que «Arrange, Act, Assert». Peut-être "Anéantir?!"
John Tobler
1
Alors maintenant, j'ai "Arranger, Assumer, Agir, Affirmer, Annihiler." Hmmm! Je complique trop les choses, hein? Peut-être que je ferais mieux de KISS off et de retourner à "Arrange, Act, and Assert!"
John Tobler
1
Peut-être utiliser un R pour réinitialiser? Je sais que ce n'est pas un A, mais cela ressemble à un pirate qui dit: Aaargh! et Reset rime avec Assert: o
Marcel Valdez Orozco
1

Jetez un œil à l'entrée de Wikipedia sur la conception par contrat . La sainte trinité Arrange-Act-Assert est une tentative d'encoder certains des mêmes concepts et vise à prouver l'exactitude du programme. De l'article:

The notion of a contract extends down to the method/procedure level; the
contract for each method will normally contain the following pieces of
information:

    Acceptable and unacceptable input values or types, and their meanings
    Return values or types, and their meanings
    Error and exception condition values or types that can occur, and their meanings
    Side effects
    Preconditions
    Postconditions
    Invariants
    (more rarely) Performance guarantees, e.g. for time or space used

Il y a un compromis entre la quantité d'efforts consacrés à la mise en place et la valeur ajoutée. AAA est un rappel utile pour les étapes minimales requises, mais ne devrait décourager personne de créer des étapes supplémentaires.

David Clarke
la source
0

Dépend de votre environnement / langue de test, mais généralement si quelque chose dans la partie Arrangement échoue, une exception est levée et le test échoue à l'afficher au lieu de démarrer la partie Act. Donc non, je n'utilise généralement pas une deuxième partie Assert.

De plus, dans le cas où votre partie Arrange est assez complexe et ne lève pas toujours une exception, vous pourriez peut-être envisager de l'envelopper dans une méthode et d'écrire un test pour elle, afin d'être sûr qu'elle n'échouera pas (sans lancer une exception).

schnaader
la source
0

Je n'utilise pas ce modèle, car je pense faire quelque chose comme:

Arrange
Assert-Not
Act
Assert

Peut-être inutile, car vous savez que vous savez que votre partie Arrangement fonctionne correctement, ce qui signifie que tout ce qui se trouve dans la partie Arrange doit également être testé ou être assez simple pour ne pas avoir besoin de tests.

En utilisant l'exemple de votre réponse:

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7)); // <-- Pointless and against DRY if there 
                                    // are unit tests for Range(int, int)
    range.encompass(7);
    assertTrue(range.includes(7));
}
Marcel Valdez Orozco
la source
J'ai bien peur que vous ne compreniez pas vraiment ma question. L'affirmation initiale ne concerne pas le test d'Arrange; il s'agit simplement de s'assurer que la loi est ce qui amène l'État à s'affirmer à la fin.
Carl Manaster
Et mon point est que, tout ce que vous mettez dans la partie Assert-Not, est déjà implicite dans la partie Arrange, car le code de la partie Arrange est minutieusement testé et vous savez déjà ce qu'il fait.
Marcel Valdez Orozco
Mais je crois qu'il y a de la valeur dans la partie Assert-Not, parce que vous dites: étant donné que la partie Arrange laisse `` le monde '' dans `` cet état '', mon `` acte '' laissera `` le monde '' dans ce `` nouvel état '' ; et si l'implémentation du code dont dépend la partie Arrange change, alors le test s'arrête également. Mais encore une fois, cela pourrait être contre DRY, car vous (devriez) également avoir des tests pour le code dont vous dépendez dans la partie Arrangement.
Marcel Valdez Orozco
Peut-être que dans les projets où il y a plusieurs équipes (ou une grande équipe) travaillant sur le même projet, une telle clause serait assez utile, sinon je la trouve inutile et redondante.
Marcel Valdez Orozco
Une telle clause serait probablement meilleure dans les tests d'intégration, les tests système ou les tests d'acceptation, où la partie Arrangement dépend généralement de plus d'un composant, et il y a plus de facteurs qui pourraient provoquer un changement inattendu de l'état initial du `` monde ''. Mais je ne vois pas de place pour cela dans les tests unitaires.
Marcel Valdez Orozco
0

Si vous voulez vraiment tout tester dans l'exemple, essayez plus de tests ... comme:

public void testIncludes7() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
}

public void testIncludes5() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(5));
}

public void testIncludes0() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(0));
}

public void testEncompassInc7() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(7));
}

public void testEncompassInc5() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(5));
}

public void testEncompassInc0() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(0));
}

Parce que sinon, il vous manque tellement de possibilités d'erreur ... par exemple, après englober, la plage n'inclut que 7, etc. un autre ensemble de tests entièrement pour essayer d'englober 5 dans la plage ... à quoi s'attendre - une exception en englobant, ou la plage à être inchangée?

Quoi qu'il en soit, le fait est que s'il y a des hypothèses dans la loi que vous voulez tester, mettez-les dans leur propre test, oui?

Andrew
la source
0

J'utilise:

1. Setup
2. Act
3. Assert 
4. Teardown

Parce qu'une configuration propre est très importante.

kame
la source