web-dev-qa-db-fra.com

Est-il OK d'avoir plusieurs assertions dans un seul test unitaire?

Dans le commentaire de cet excellent article , Roy Osherove a mentionné le projet OAPT qui est conçu pour exécuter chaque assertion dans un seul test.

Ce qui suit est écrit sur la page d'accueil du projet:

Les tests unitaires appropriés devraient échouer pour exactement une raison, c'est pourquoi vous devriez utiliser une assertion par test unitaire.

Et, aussi, Roy a écrit dans les commentaires:

Ma ligne directrice est généralement que vous testez un CONCEPT logique par test. vous pouvez avoir plusieurs assertions sur le même objet. ils seront généralement le même concept testé.

Je pense que, dans certains cas, plusieurs assertions sont nécessaires (par exemple Guard Assertion ), mais en général, j'essaie d'éviter cela. Quel est ton opinion? Veuillez fournir un exemple réel où plusieurs assertions sont vraiment nécessaires.

430
Restuta

Je ne pense pas que ce soit nécessairement une mauvaise chose , mais je pense que nous devrions nous efforcer de n'avoir que des assertions uniques dans nos tests. Cela signifie que vous écrivez beaucoup plus de tests et nos tests finiraient par ne tester qu'une seule chose à la fois.

Cela dit, je dirais que la moitié de mes tests n'ont en fait qu'une seule affirmation. Je pense que cela ne devient un code (test?) Odeur que lorsque vous avez environ cinq assertions ou plus dans votre test.

Comment résolvez-vous plusieurs assertions?

246
Jaco Pretorius

Les tests doivent échouer pour une seule raison, mais cela ne signifie pas toujours qu'il ne doit y avoir qu'une seule instruction Assert. À mon humble avis, il est plus important de conserver le modèle " Arrange, Act, Assert ".

La clé est que vous n'avez qu'une seule action, puis vous inspectez les résultats de cette action à l'aide d'asserts. Mais c'est "Arrange, Act, Assert, Fin du test ". Si vous êtes tenté de continuer les tests en effectuant une autre action et d'autres assertions par la suite, faites-en plutôt un test distinct.

Je suis heureux de voir plusieurs déclarations assert qui font partie du test de la même action. par exemple.

[Test]
public void ValueIsInRange()
{
  int value = GetValueToTest();

  Assert.That(value, Is.GreaterThan(10), "value is too small");
  Assert.That(value, Is.LessThan(100), "value is too large");
} 

ou

[Test]
public void ListContainsOneValue()
{
  var list = GetListOf(1);

  Assert.That(list, Is.Not.Null, "List is null");
  Assert.That(list.Count, Is.EqualTo(1), "Should have one item in list");
  Assert.That(list[0], Is.Not.Null, "Item is null");
} 

Vous pourriez les combiner en une seule assertion, mais ce n'est pas la même chose que d'insister pour que vous ou doit. Il n'y a aucune amélioration à les combiner.

par exemple. Le premier pourrait être

Assert.IsTrue((10 < value) && (value < 100), "Value out of range"); 

Mais ce n'est pas mieux - le message d'erreur est moins spécifique et n'a pas d'autres avantages. Je suis sûr que vous pouvez penser à d'autres exemples où la combinaison de deux ou trois assertions (ou plus) en une seule grosse condition booléenne rend la lecture plus difficile, plus difficile à modifier et plus difficile à comprendre pourquoi elle a échoué. Pourquoi faire cela juste pour le bien d'une règle?

[~ # ~] nb [~ # ~] : Le code que j'écris ici est C # avec NUnit, mais les principes se maintiendront avec d'autres langages et cadres. La syntaxe peut également être très similaire.

314
Anthony

Je n'ai jamais pensé que plus d'une affirmation était une mauvaise chose.

Je le fais tout le temps:

public void ToPredicateTest()
{
    ResultField rf = new ResultField(ResultFieldType.Measurement, "name", 100);
    Predicate<ResultField> p = (new ConditionBuilder()).LessThanConst(400)
                                                       .Or()
                                                       .OpenParenthesis()
                                                       .GreaterThanConst(500)
                                                       .And()
                                                       .LessThanConst(1000)
                                                       .And().Not()
                                                       .EqualsConst(666)
                                                       .CloseParenthesis()
                                                       .ToPredicate();
    Assert.IsTrue(p(ResultField.FillResult(rf, 399)));
    Assert.IsTrue(p(ResultField.FillResult(rf, 567)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 400)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 666)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 1001)));

    Predicate<ResultField> p2 = (new ConditionBuilder()).EqualsConst(true).ToPredicate();

    Assert.IsTrue(p2(new ResultField(ResultFieldType.Confirmation, "Is True", true)));
    Assert.IsFalse(p2(new ResultField(ResultFieldType.Confirmation, "Is False", false)));
}

Ici, j'utilise plusieurs assertions pour m'assurer que des conditions complexes peuvent être transformées en prédicat attendu.

Je ne teste qu'une seule unité (la méthode ToPredicate), mais je couvre tout ce à quoi je peux penser dans le test.

85
Matt Ellen

Lorsque j'utilise des tests unitaires pour valider un comportement de haut niveau, je mets absolument plusieurs assertions en un seul test. Voici un test que j'utilise actuellement pour un code de notification d'urgence. Le code qui s'exécute avant le test met le système dans un état où si le processeur principal s'exécute, une alarme est envoyée.

@Test
public void testAlarmSent() {
    assertAllUnitsAvailable();
    assertNewAlarmMessages(0);

    pulseMainProcessor();

    assertAllUnitsAlerting();
    assertAllNotificationsSent();
    assertAllNotificationsUnclosed();
    assertNewAlarmMessages(1);
}

Il représente les conditions qui doivent exister à chaque étape du processus pour que je puisse être sûr que le code se comporte comme je l'espère. Si une seule assertion échoue, je me fiche que les autres ne soient même pas exécutées; parce que l'état du système n'est plus valide, ces assertions ultérieures ne me diraient rien de valable. * Si assertAllUnitsAlerting() a échoué, je ne saurais pas quoi faire de assertAllNotificationSent() success OR échec jusqu'à ce que je détermine la cause de l'erreur précédente et que je la corrige.

(* - D'accord, ils pourraient éventuellement être utiles pour déboguer le problème. Mais les informations les plus importantes, que le test a échoué, ont déjà été reçues.)

21
BlairHippo

Une autre raison pour laquelle je pense que plusieurs assertions dans une méthode n'est pas une mauvaise chose est décrite dans le code suivant:

class Service {
    Result process();
}

class Result {
    Inner inner;
}

class Inner {
    int number;
}

Dans mon test, je veux simplement tester que service.process() renvoie le nombre correct dans les instances de classe Inner.

Au lieu de tester ...

@Test
public void test() {
    Result res = service.process();
    if ( res != null && res.getInner() != null ) Assert.assertEquals( ..., res.getInner() );
}

Je fais

@Test
public void test() {
    Result res = service.process();
    Assert.notNull(res);
    Assert.notNull(res.getInner());
    Assert.assertEquals( ..., res.getInner() );
}
8
Betlista

Je pense qu'il existe de nombreux cas où l'écriture de plusieurs assertions est valide dans la règle selon laquelle un test ne doit échouer que pour une seule raison.

Par exemple, imaginez une fonction qui analyse une chaîne de date:

function testParseValidDateYMD() {
    var date = Date.parse("2016-01-02");

    Assert.That(date.Year).Equals(2016);
    Assert.That(date.Month).Equals(1);
    Assert.That(date.Day).Equals(0);
}

Si le test échoue, c'est pour une raison, l'analyse est incorrecte. Si vous soutenez que ce test peut échouer pour trois raisons différentes, vous serez à mon humble avis trop précis dans votre définition de "une seule raison".

6
Pete

Je ne connais aucune situation où ce serait une bonne idée d'avoir plusieurs assertions à l'intérieur de la méthode [Test] elle-même. La raison principale pour laquelle les gens aiment avoir plusieurs assertions est qu'ils essaient d'avoir une classe [TestFixture] pour chaque classe testée. Au lieu de cela, vous pouvez diviser vos tests en plusieurs classes [TestFixture]. Cela vous permet de voir plusieurs façons dont le code n'a peut-être pas réagi de la manière attendue, au lieu de celle où la première assertion a échoué. Pour ce faire, vous disposez d'au moins un répertoire par classe en cours de test avec de nombreuses classes [TestFixture] à l'intérieur. Chaque classe [TestFixture] serait nommée d'après l'état spécifique d'un objet que vous testerez. La méthode [SetUp] mettra l'objet dans l'état décrit par le nom de classe. Ensuite, vous disposez de plusieurs méthodes [Test], chacune affirmant différentes choses que vous attendez pour être vraies, compte tenu de l'état actuel de l'objet. Chaque méthode [Test] est nommée d'après la chose qu'elle affirme, sauf peut-être qu'elle pourrait être nommée d'après le concept au lieu d'une simple lecture en anglais du code. Ensuite, chaque implémentation de la méthode [Test] n'a besoin que d'une seule ligne de code où elle affirme quelque chose. Un autre avantage de cette approche est qu'elle rend les tests très lisibles car il devient assez clair ce que vous testez et ce que vous attendez simplement en regardant les noms de classe et de méthode. Cela évoluera également mieux lorsque vous commencerez à réaliser tous les petits cas Edge que vous souhaitez tester et que vous trouverez des bogues.

Cela signifie généralement que la dernière ligne de code à l'intérieur de la méthode [SetUp] doit stocker une valeur de propriété ou renvoyer une valeur dans une variable d'instance privée de [TestFixture]. Ensuite, vous pouvez affirmer plusieurs choses différentes sur cette variable d'instance à partir de différentes méthodes [Test]. Vous pouvez également faire des affirmations sur les différentes propriétés de l'objet à tester définies maintenant qu'il est dans l'état souhaité.

Parfois, vous devez faire des affirmations en cours de route lorsque vous mettez l'objet à tester dans l'état souhaité afin de vous assurer que vous n'avez pas gâché avant de placer l'objet dans l'état souhaité. Dans ce cas, ces assertions supplémentaires doivent apparaître dans la méthode [SetUp]. Si quelque chose se passe mal à l'intérieur de la méthode [SetUp], il sera clair que quelque chose n'allait pas avec le test avant que l'objet n'atteigne l'état souhaité que vous vouliez tester.

Un autre problème que vous pouvez rencontrer est que vous testez peut-être une exception que vous attendiez. Cela peut vous inciter à ne pas suivre le modèle ci-dessus. Cependant, il peut toujours être atteint en interceptant l'exception à l'intérieur de la méthode [SetUp] et en la stockant dans une variable d'instance. Cela vous permettra d'affirmer différentes choses sur l'exception, chacune dans sa propre méthode [Test]. Vous pouvez ensuite également affirmer d'autres choses sur l'objet en cours de test pour vous assurer qu'il n'y a pas d'effets secondaires involontaires de l'exception levée.

Exemple (ce serait divisé en plusieurs fichiers):

namespace Tests.AcctTests
{
    [TestFixture]
    public class no_events
    {
        private Acct _acct;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
        }

        [Test]
        public void balance_0() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_0
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(0m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_negative
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(-0.01m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }
}
3
still_dreaming_1

Si vous avez plusieurs assertions dans une seule fonction de test, je m'attends à ce qu'elles soient directement pertinentes pour le test que vous effectuez. Par exemple,

@Test
test_Is_Date_segments_correct {

   // It is okay if you have multiple asserts checking dd, mm, yyyy, hh, mm, ss, etc. 
   // But you would not have any assert statement checking if it is string or number,
   // that is a different test and may be with multiple or single assert statement.
}

Avoir beaucoup de tests (même si vous pensez que c'est probablement une surpuissance) n'est pas une mauvaise chose. Vous pouvez affirmer qu'il est plus important d'avoir les tests vitaux et les plus essentiels. Donc, lorsque vous affirmez, assurez-vous que vos déclarations d'assertion sont correctement placées plutôt que de vous soucier de plusieurs assertions trop. Si vous en avez besoin de plusieurs, utilisez-en plusieurs.

2
hagubear

Avoir plusieurs assertions dans le même test n'est un problème que lorsque le test échoue. Ensuite, vous devrez peut-être déboguer le test ou analyser l'exception pour savoir quelle assertion échoue. Avec une assertion dans chaque test, il est généralement plus facile de déterminer ce qui ne va pas.

Je ne peux pas penser à un scénario où plusieurs assertions sont vraiment nécessaires, car vous pouvez toujours les réécrire en plusieurs conditions dans la même assertion. Il peut cependant être préférable si vous avez par exemple plusieurs étapes pour vérifier les données intermédiaires entre les étapes plutôt que de risquer que les étapes ultérieures se bloquent en raison d'une mauvaise entrée.

2
Guffa

Si votre test échoue, vous ne saurez pas non plus si les assertions suivantes seront rompues. Souvent, cela signifie que vous manquerez des informations précieuses pour déterminer la source du problème. Ma solution consiste à utiliser une assertion mais avec plusieurs valeurs:

String actual = "val1="+val1+"\nval2="+val2;
assertEquals(
    "val1=5\n" +
    "val2=hello"
    , actual
);

Cela me permet de voir toutes les assertions ratées à la fois. J'utilise plusieurs lignes car la plupart des IDE affichent les différences de chaîne côte à côte dans une boîte de dialogue de comparaison.

2
Aaron Digulla

Le test unitaire a pour objectif de vous fournir autant d'informations que possible sur ce qui échoue, mais aussi d'aider à identifier avec précision les problèmes les plus fondamentaux en premier. Lorsque vous savez logiquement qu'une assertion échouera étant donné qu'une autre assertion échoue ou en d'autres termes qu'il existe une relation de dépendance entre le test, il est logique de les rouler en plusieurs assertions dans un même test. Cela a l'avantage de ne pas joncher les résultats des tests avec des échecs évidents qui auraient pu être éliminés si nous avions renoncé à la première affirmation dans un seul test. Dans le cas où cette relation n'existe pas, la préférence serait naturellement alors de séparer ces assertions en tests individuels, car sinon, trouver ces échecs nécessiterait plusieurs itérations de tests pour résoudre tous les problèmes.

Si vous concevez également les unités/classes de telle manière que des tests trop complexes devraient être écrits, cela réduira la charge lors des tests et favorisera probablement une meilleure conception.

1
jpierson

Oui, il est correct d'avoir plusieurs assertions tant que un test qui échoue vous donne suffisamment d'informations pour pouvoir diagnostiquer l'échec. Cela dépendra de ce que vous testez et des modes de défaillance.

Les tests unitaires appropriés doivent échouer pour exactement une raison, c'est pourquoi vous devez utiliser une assertion par test unitaire.

Je n'ai jamais trouvé de telles formulations utiles (qu'une classe devrait avoir une raison de changer est un exemple d'un adage aussi inutile). Considérons une affirmation selon laquelle deux chaînes sont égales, cela équivaut sémantiquement à affirmer que la longueur des deux chaînes est la même et que chaque caractère de l'index correspondant est égal.

Nous pourrions généraliser et dire que tout système d'assertions multiples pourrait être réécrit en une seule assertion, et toute assertion unique pourrait être décomposée en un ensemble d'assertions plus petites.

Donc, concentrez-vous uniquement sur la clarté du code et la clarté des résultats des tests, et laissez-les guider le nombre d'assertions que vous utilisez plutôt que l'inverse.

1
CurtainDog

Cette question est liée au problème classique d'équilibrage entre les problèmes de code de spaghetti et de lasagne.

Avoir plusieurs assertions pourrait facilement entraîner le problème des spaghettis où vous n'avez pas une idée du sujet du test, mais avoir une assertion unique par test pourrait rendre votre test tout aussi illisible ayant plusieurs tests dans une grande lasagne, ce qui rend impossible de trouver quel test fait quoi .

Il y a quelques exceptions, mais dans ce cas, garder le pendule au milieu est la réponse.

0
gsf

La réponse est très simple - si vous testez une fonction qui change plus d'un attribut, du même objet, ou même deux objets différents, et l'exactitude de la fonction dépend des résultats de tous ces changements, alors vous voulez affirmer que chacun de ces changements a été correctement effectué!

J'ai l'idée d'un concept logique, mais la conclusion inverse dirait qu'aucune fonction ne doit jamais changer plus d'un objet. Mais c'est impossible à mettre en œuvre dans tous les cas, selon mon expérience.

Prenez le concept logique d'une transaction bancaire - retirer un montant d'un compte bancaire dans la plupart des cas DOIT inclure l'ajout de ce montant à un autre compte. Vous ne voulez JAMAIS séparer ces deux choses, elles forment une unité atomique. Vous voudrez peut-être créer deux fonctions (retirer/ajouter de l'argent) et ainsi écrire deux tests unitaires différents - en plus. Mais ces deux actions doivent avoir lieu au sein d'une même transaction et vous voulez également vous assurer que la transaction fonctionne. Dans ce cas, il ne suffit tout simplement pas de s'assurer que les étapes individuelles ont réussi. Vous devez vérifier les deux comptes bancaires dans votre test.

Il peut y avoir des exemples plus complexes que vous ne testeriez pas dans un test unitaire, en premier lieu, mais à la place dans un test d'intégration ou d'acceptation. Mais ces frontières sont fluides, à mon humble avis! Ce n'est pas si facile à décider, c'est une question de circonstances et peut-être de préférence personnelle. Retirer de l'argent d'un compte et l'ajouter à un autre compte reste une fonction très simple et certainement un candidat pour les tests unitaires.

0
cslotty