Ai-je vraiment besoin d'un framework de test unitaire?

19

Actuellement à mon travail, nous avons une grande suite de tests unitaires pour notre application C ++. Cependant, nous n'utilisons pas de cadre de test unitaire. Ils utilisent simplement une macro C qui englobe essentiellement une assertion et un cout. Quelque chose comme:

VERIFY(cond) if (!(cond)) {std::cout << "unit test failed at " << __FILE__ << "," << __LINE__; asserst(false)}

Ensuite, nous créons simplement des fonctions pour chacun de nos tests comme

void CheckBehaviorYWhenXHappens()
{
    // a bunch of code to run the test
    //
    VERIFY(blah != blah2);
    // more VERIFY's as needed
}

Notre serveur CI détecte «le test unitaire a échoué» et échoue à la génération, en envoyant le message par e-mail aux développeurs.

Et si nous avons un code de configuration en double, nous le refactorisons simplement comme nous le ferions avec tout autre code en double que nous aurions en production. Nous l'enveloppons derrière des fonctions d'assistance, nous faisons en sorte que certaines classes de test mettent en place des scénarios fréquemment utilisés.

Je sais qu'il existe des frameworks comme CppUnit et boost test unitaire. Je me demande quelle valeur cela ajoute-t-il? Suis-je absent de ce que cela apporte à la table? Y a-t-il quelque chose d'utile que je pourrais gagner d'eux? J'hésite à ajouter une dépendance à moins qu'elle n'apporte une réelle valeur d'autant plus qu'il semble que ce que nous avons est simple et fonctionne bien.

Doug T.
la source

Réponses:

8

Comme d'autres l'ont déjà dit, vous avez déjà votre propre cadre simple et fait maison.

Il semble insignifiant d'en faire un. Cependant, il existe d'autres fonctionnalités d'un framework de test unitaire qui ne sont pas si faciles à implémenter, car elles nécessitent une connaissance avancée du langage. Les fonctionnalités dont j'ai généralement besoin d'un framework de test et qui ne sont pas si faciles à utiliser sont:

  • Collecte automatique des cas de test. C'est-à-dire que la définition d'une nouvelle méthode de test devrait être suffisante pour la faire exécuter. JUnit collecte automatiquement toutes les méthodes dont les noms commencent par test, NUnit a l' [Test]annotation, Boost.Test utilise les macros BOOST_AUTO_TEST_CASEet BOOST_FIXTURE_TEST_CASE.

    C'est surtout de la commodité, mais chaque commodité que vous pouvez obtenir améliore les chances que les développeurs écrivent réellement les tests qu'ils devraient et qu'ils les connectent correctement. Si vous avez de longues instructions, quelqu'un en manquera une partie maintenant et peut-être que certains tests ne seront pas en cours d'exécution et personne ne le remarquera.

  • Possibilité d'exécuter des cas de test sélectionnés, sans modifier le code ni recompiler. Tout cadre décent de tests unitaires vous permet de spécifier les tests que vous souhaitez exécuter en ligne de commande. Si vous souhaitez déboguer sur des tests unitaires (c'est le point le plus important pour de nombreux développeurs), vous devez pouvoir en sélectionner quelques-uns à exécuter, sans modifier le code partout.

    Supposons que vous venez de recevoir le rapport de bogue # 4211 et qu'il peut être reproduit avec le test unitaire. Donc, vous en écrivez un, mais vous devez dire au coureur d'exécuter uniquement ce test, afin que vous puissiez déboguer ce qui ne va pas.

  • Possibilité de marquer les tests des échecs attendus, par cas de test, sans modifier les contrôles eux-mêmes. Nous avons en fait changé de frameworks pour obtenir celui-ci.

    Toute suite de tests de taille décente aura des tests, qui échouent parce que les fonctionnalités qu'ils testent n'ont pas encore été implémentées, n'étaient pas encore terminées, personne n'a eu le temps de les corriger ou quelque chose. Sans possibilité de marquer les tests comme des échecs attendus, vous ne remarquerez pas un autre échec lorsqu'il y en a régulièrement, donc les tests cessent de servir leur objectif principal.

Jan Hudec
la source
merci, je pense que c'est la meilleure réponse. En ce moment, ma macro fait son travail, mais je ne peux faire aucune des fonctionnalités que vous mentionnez.
Doug T.
1
@Jan Hudec "C'est surtout la commodité, mais chaque commodité que vous pouvez obtenir améliore les chances que les développeurs écrivent réellement les tests qu'ils devraient et qu'ils les connectent correctement."; Tous les cadres de test sont (1) simples à installer, contiennent souvent des instructions d'installation plus obsolètes ou non exhaustives que des instructions valides à jour; (2) si vous vous engagez directement sur un framework de test, sans interface au milieu, vous êtes marié avec lui, changer de framework n'est pas toujours facile.
Dmitry
@Jan Hudec Si nous nous attendons à ce que plus de personnes écrivent des tests unitaires, nous devons avoir plus de résultats sur google pour "Qu'est-ce qu'un test unitaire", que "Qu'est-ce qu'un test unitaire". Il est inutile de faire des tests unitaires si vous ne savez pas ce qu'est un test unitaire indépendamment des cadres de test unitaire ou de la définition du test unitaire. Vous ne pouvez pas faire de test unitaire à moins que vous ayez une bonne compréhension de ce qu'est un test unitaire, sinon il est inutile de faire un test unitaire.
Dmitry
Je n'achète pas cet argument de commodité. Il est très difficile d'écrire du code de test si vous quittez le monde trivial des exemples. Toutes ces maquettes, configurations, bibliothèques, programmes de serveurs de maquettes externes, etc. Ils nécessitent tous que vous connaissiez le cadre de test de l'intérieur.
Lothar
@Lothar, oui, tout cela demande beaucoup de travail et beaucoup à apprendre, mais le fait de devoir écrire plusieurs fois un simple passe-partout parce que vous manquez de quelques utilitaires utiles rend le travail beaucoup moins agréable et cela fait une différence notable d'efficacité.
Jan Hudec
27

On dirait que vous utilisez déjà un cadre, un fait maison.

Quelle est la valeur ajoutée des frameworks les plus populaires? Je dirais que la valeur ajoutée est que lorsque vous devez échanger du code avec des personnes extérieures à votre entreprise, vous pouvez le faire, car il est basé sur le cadre qui est connu et largement utilisé .

Un framework fait maison, en revanche, vous oblige à ne jamais partager votre code, ou à fournir le framework lui-même, ce qui peut devenir lourd avec la croissance du framework lui-même.

Si vous donnez votre code à un collègue tel quel, sans explication ni cadre de test unitaire, il ne pourrait pas le compiler.

Un deuxième inconvénient des frameworks maison est la compatibilité . Les frameworks de tests unitaires populaires tendent à garantir la compatibilité avec différents IDE, systèmes de contrôle de version, etc. vers un nouvel IDE ou un nouveau VCS? Réinventerez-vous la roue?

Enfin, des cadres plus grands offrent un plus grand nombre de fonctionnalités que vous devrez peut-être implémenter un jour dans votre propre cadre. Assert.AreEqual(expected, actual)n'est pas toujours suffisant. Et si vous devez:

  • mesurer la précision?

    Assert.AreEqual(3.1415926535897932384626433832795, actual, 25)
    
  • annuler le test s'il fonctionne trop longtemps? Réimplémenter un délai d'attente peut ne pas être simple, même dans les langages qui facilitent la programmation asynchrone.

  • tester une méthode qui attend qu'une exception soit levée?

  • avoir un code plus élégant?

    Assert.Verify(a == null);
    

    est bien, mais n'est-il pas plus expressif de votre intention d'écrire la ligne suivante à la place?

    Assert.IsNull(a);
    
Arseni Mourzenko
la source
Le "framework" que nous utilisons est tout dans un très petit fichier d'en-tête et suit la sémantique d'assert. Je ne m'inquiète donc pas trop des inconvénients que vous citez.
Doug
4
Je considère que les assertions sont la partie la plus triviale du framework de test. Le coureur qui collecte et exécute les cas de test et vérifie les résultats est la partie importante non triviale.
Jan Hudec
@Jan je ne suis pas tout à fait suivre. Mon runner est une routine principale commune à tous les programmes C ++. Un exécuteur de framework de tests unitaires fait-il quelque chose de plus sophistiqué et utile?
Doug
1
Votre framework ne permet que la sémantique des assertions et l'exécution de tests dans une méthode principale ... jusqu'à présent. Attendez simplement de devoir regrouper vos assertions en plusieurs scénarios, regroupez les scénarios liés en fonction de données initialisées, etc.
James Kingsbery
@DougT .: Oui, un exécuteur de framework de test unitaire décent fait des choses utiles plus sophistiquées. Voir ma réponse complète.
Jan Hudec
4

Comme d'autres l'ont déjà dit, vous avez déjà votre propre cadre fait maison.

La seule raison pour laquelle je peux voir l'utilisation d'un autre cadre de test serait du point de vue de la «connaissance commune» de l'industrie. Les nouveaux développeurs n'auraient pas à apprendre votre chemin à la maison (bien que cela semble très simple).

De plus, d'autres frameworks de test peuvent avoir plus de fonctionnalités dont vous pourriez profiter.

ozz
la source
1
D'accord. Si vous ne rencontrez pas de limites avec votre stratégie de test actuelle, je ne vois pas de raison de changer. Un bon framework fournirait probablement une meilleure organisation et des capacités de reporting, mais vous auriez à justifier le travail supplémentaire requis pour intégrer avec votre base de code (y compris votre système de build).
TMN
3

Vous avez déjà un framework même s'il est simple.

Les principaux avantages d'un cadre plus large tel que je les vois sont la possibilité d'avoir de nombreux types d'assertions (comme les assertions), un ordre logique pour les tests unitaires et la possibilité d'exécuter uniquement un sous-ensemble de tests unitaires à un temps. De plus, le modèle des tests xUnit est assez agréable à suivre si vous le pouvez - par exemple celui de setUP () et tearDown (). Bien sûr, cela vous enferme dans ledit cadre. Notez que certains frameworks ont une meilleure intégration factice que d'autres - google maquette et test par exemple.

Combien de temps vous faudra-t-il pour refactoriser tous vos tests unitaires vers un nouveau framework? Des jours ou quelques semaines en valent peut-être la peine mais plus peut-être moins.

Sardathrion - Rétablir Monica
la source
2

De mon point de vue, vous avez tous les deux l'avantage et vous êtes "désavantagé" (sic).

L'avantage est que vous disposez d'un système avec lequel vous vous sentez à l'aise et qui fonctionne pour vous. Vous êtes heureux que cela confirme la validité de votre produit, et vous ne trouveriez probablement aucune valeur commerciale en essayant de changer tous vos tests pour quelque chose qui utilise un cadre différent. Si vous pouvez refactoriser votre code et que vos tests détectent les modifications - ou mieux encore, si vous pouvez modifier vos tests et que votre code existant échoue aux tests jusqu'à ce qu'il soit refactorisé, alors toutes vos bases sont couvertes. Pourtant...

L'un des avantages d'avoir une API de test unitaire bien conçue est qu'il y a beaucoup de support natif dans la plupart des IDE modernes. Cela n'affectera pas le VI noyau dur et les utilisateurs emacs qui ricanent les utilisateurs de Visual Studio, mais pour ceux qui utilisent un bon IDE, vous avez la possibilité de déboguer vos tests et de les exécuter dans l'IDE lui-même. C'est bien, mais il y a un avantage encore plus grand en fonction du framework que vous utilisez, et c'est dans le langage utilisé pour tester votre code.

Quand je dis langage , je ne parle pas d'un langage de programmation, mais plutôt d'un ensemble de mots riches enveloppés dans une syntaxe fluide qui fait lire le code de test comme une histoire. En particulier, je suis devenu un défenseur de l'utilisation des cadres BDD . Mon API personnelle DotNet BDD est StoryQ, mais il y en a plusieurs autres avec le même objectif de base, qui est de retirer un concept d'un document d'exigences et de l'écrire dans le code d'une manière similaire à la façon dont il est écrit dans la spécification. Les très bonnes API vont cependant encore plus loin, en interceptant chaque instruction individuelle dans un test et en indiquant si cette instruction s'est exécutée avec succès ou a échoué. Ceci est incroyablement utile, car vous pouvez voir l'intégralité du test exécuté sans revenir tôt, ce qui signifie que vos efforts de débogage deviennent incroyablement efficaces car vous n'avez besoin de concentrer votre attention que sur les parties du test qui ont échoué, sans avoir besoin de décoder l'appel entier séquence. L'autre chose intéressante est que la sortie de test vous montre toutes ces informations,

À titre d'exemple de ce dont je parle, comparez les éléments suivants:

Utilisation d'assertions:

Assert(variable_A == expected_value_1); // if this fails...
Assert(variable_B == expected_value_2); // ...this will not execute
Assert(variable_C == expected_value_3); // ...and nor will this!

Utilisation d'une API BDD fluide: (Imaginez que les bits en italique sont essentiellement des pointeurs de méthode)

WithScenario("Test Scenario")
    .Given(*AConfiguration*) // each method
    .When(*MyMethodToTestIsCalledWith*, variable_A, variable_B, variable_C) // in the
    .Then(*ExpectVariableAEquals*, expected_value_1) // Scenario will
        .And(*ExpectVariableBEquals*, expected_value_2) // indicate if it has
        .And(*ExpectVariableCEquals*, expected_value_3) // passed or failed execution.
    .Execute();

Maintenant, la syntaxe BDD est plus longue et plus verbeuse, et ces exemples sont terriblement artificiels, mais pour les situations de test très complexes où beaucoup de choses changent dans un système en raison d'un comportement système donné, la syntaxe BDD vous offre une une description de ce que vous testez et de la façon dont votre configuration de test a été définie, et vous pouvez montrer ce code à un non-programmeur et il comprendra instantanément ce qui se passe. De plus, si "variable_A" échoue au test dans les deux cas, l'exemple Asserts ne s'exécutera pas après la première assertion tant que vous n'aurez pas résolu le problème, tandis que l'API BDD exécutera tour à tour chaque méthode appelée dans la chaîne et indiquera laquelle certaines parties de la déclaration étaient erronées.

Personnellement, je trouve que cette approche fonctionne beaucoup mieux que les frameworks xUnit plus traditionnels dans le sens où le langage de test est le même langage que vos clients parleront de leurs exigences logiques. Malgré tout, j'ai réussi à utiliser les frameworks xUnit dans un style similaire sans avoir besoin d'inventer une API de test complète pour soutenir mes efforts, et bien que les assertions se court-circuitent toujours efficacement, elles lisent plus proprement. Par exemple:

Utilisation de Nunit :

[Test]
void TestMyMethod()
{
    const int theExpectedValue = someValue;

    GivenASetupToTestMyMethod();

    var theActualValue = WhenIExecuteMyMethodToTest();

    Assert.That(theActualValue, Is.EqualTo(theExpectedValue)); // nice, but it's not BDD
}

Si vous décidez d'explorer l'utilisation d'une API de test unitaire, mon conseil est d'expérimenter avec un grand nombre d'API différentes pendant un petit moment, et de garder l'esprit ouvert sur votre approche. Bien que je plaide personnellement pour le BDD, vos propres besoins commerciaux peuvent nécessiter quelque chose de différent selon la situation de votre équipe. La clé est cependant d'éviter de deviner votre système existant. Vous pouvez toujours prendre en charge vos tests existants avec quelques tests en utilisant une autre API si nécessaire, mais je ne recommanderais certainement pas une énorme réécriture de test juste pour que tout soit pareil. Comme le code hérité tombe en panne, vous pouvez facilement le remplacer, ainsi que ses tests, par du nouveau code, et tester à l'aide d'une API alternative, et cela sans avoir à investir dans un effort majeur qui ne vous donnera pas nécessairement une réelle valeur commerciale. Quant à l'utilisation d'une API de test unitaire,

S.Robins
la source
1

Ce que vous avez est simple et fait le travail. Si cela fonctionne pour vous, tant mieux. Vous n'avez pas besoin d' un framework de tests unitaires traditionnel, et j'hésiterais à passer au travail de portage d'une bibliothèque existante de tests unitaires vers un nouveau framework. Je pense que la plus grande valeur des cadres de tests unitaires est de réduire la barrière à l'entrée; vous venez de commencer à écrire des tests, car le framework est déjà en place. Vous avez dépassé ce point, vous n'obtiendrez donc pas cet avantage.

L'autre avantage de l'utilisation d'un cadre général - et c'est un avantage mineur, OMI - est que les nouveaux développeurs peuvent déjà être à jour sur le cadre que vous utilisez, et nécessiteront donc moins de formation. Dans la pratique, avec une approche simple comme celle que vous avez décrite, cela ne devrait pas être un gros problème.

De plus, la plupart des frameworks traditionnels ont certaines fonctionnalités que votre framework peut avoir ou non. Ces fonctionnalités réduisent le code de plomberie et facilitent l'écriture des cas de test plus rapidement et plus facilement:

  • Exécution automatique des cas de test, en utilisant des conventions de dénomination, des annotations / attributs, etc.
  • Diverses assertions plus spécifiques, afin que vous n'ayez pas à écrire de logique conditionnelle pour toutes vos assertions ou à intercepter des exceptions pour affirmer leur type.
  • Catégorisation des cas de test, afin que vous puissiez facilement en exécuter des sous-ensembles.
Adam Jaskiewicz
la source