Qu’est-ce qu’un bon test d’unité?

Je suis sûr que la plupart d’entre vous écrivez beaucoup de tests automatisés et que vous avez également rencontré des problèmes courants lors des tests unitaires.

Ma question est la suivante: suivez-vous des règles de conduite pour écrire des tests afin d’éviter des problèmes à l’avenir? Pour être plus précis: quelles sont les propriétés des bons tests unitaires ou comment écrivez-vous vos tests?

Les suggestions agnostiques sont encouragées.

Permettez-moi de commencer en branchant des sources – Pragmatic Unit Testing en Java avec JUnit (Il y a aussi une version avec C # -Nunit .. mais j’ai celle-ci … son agnostique pour la plupart. Recommandé.)

Les bons tests devraient être un voyage (l’acronyme n’est pas assez collant – j’ai une copie imprimée du cheatsheet dans le livre que j’ai dû sortir pour m’assurer que j’ai bien compris ..)

  • Automatique : l’appel des tests ainsi que la vérification des résultats pour PASS / FAIL doivent être automatiques
  • Approfondie : couverture; Bien que les bogues aient tendance à se regrouper autour de certaines régions du code, assurez-vous de tester tous les chemins d’access et scénarios clés. Utilisez des outils si vous devez connaître des régions non testées
  • Répétable : Les tests doivent produire les mêmes résultats à chaque fois. Les tests ne doivent pas s’appuyer sur des parameters incontrôlables.
  • Indépendant : très important.
    • Les tests ne doivent tester qu’une seule chose à la fois. Les assertions multiples sont acceptables tant qu’elles testent toutes une caractéristique / un comportement. Lorsqu’un test échoue, il doit identifier l’emplacement du problème.
    • Les tests ne doivent pas s’appuyer l’un sur l’autre – isolés. Aucune hypothèse concernant l’ordre d’exécution des tests. Assurer une «ardoise propre» avant chaque test en utilisant la configuration / la suppression appropriée
  • Professionnel : À long terme, vous aurez autant de code de test que de production (sinon plus), donc suivez les mêmes normes de bon design pour votre code de test. Classes de méthodes bien factorisées avec des noms révélateurs d’intention, pas de duplication, tests avec de bons noms, etc.

  • Les bons tests sont également rapides . tout test qui prend plus d’une demi-seconde à exécuter .. doit être travaillé. Plus la suite de tests est longue, moins elle sera exécutée. Plus le développeur essaiera de changer de méthode entre les exécutions, si quelque chose se brise, il faudra plus de temps pour déterminer quel changement en était la cause.

Mise à jour 2010-08:

  • Lisible : ceci peut être considéré comme faisant partie du professionnel – mais cela ne peut pas être assez souligné. Un test acide consisterait à trouver quelqu’un qui ne fait pas partie de votre équipe et à lui demander de comprendre le comportement sous test en quelques minutes. Les tests doivent être maintenus, tout comme le code de production, ce qui facilite la lecture, même si cela demande plus d’efforts. Les tests doivent être symésortingques (suivre un modèle) et concis (tester un comportement à la fois). Utilisez une convention de dénomination cohérente (par exemple, le style TestDox). Évitez d’encombrer le test avec des «détails accessoires». Devenez un minimaliste.

En dehors de cela, la plupart des autres sont des lignes direcsortingces qui réduisent le travail à faible bénéfice: par exemple, «Ne testez pas le code que vous ne possédez pas» (par exemple, des DLL tierces). Ne vous lancez pas dans les tests et les tests. Gardez un œil sur le rapport coût / bénéfice ou la probabilité de défaut.

  1. N’écrivez pas de tests gigantesques. Comme le suggère l’unité de «test unitaire», faites en sorte que chacun soit aussi atomique que possible. Si vous devez, créez des conditions préalables à l’aide d’objects fictifs, plutôt que de recréer manuellement une trop grande partie de l’environnement utilisateur type.
  2. Ne testez pas les choses qui fonctionnent évidemment. Évitez de tester les classes à partir d’un fournisseur tiers, en particulier celui qui fournit les API principales du framework que vous codez. Par exemple, ne testez pas l’ajout d’un élément à la classe Hashtable du fournisseur.
  3. Pensez à utiliser un outil de couverture de code tel que NCover pour vous aider à découvrir les cas limites que vous n’avez pas encore testés.
  4. Essayez d’écrire le test avant l’implémentation. Considérez le test comme une spécification que votre implémentation respectera. Cf. également un développement axé sur les comportements, une twig plus spécifique du développement axé sur les tests.
  5. Être cohérent. Si vous écrivez seulement des tests pour une partie de votre code, ce n’est guère utile. Si vous travaillez en équipe et que certains ou tous les autres n’écrivent pas de tests, ce n’est pas très utile non plus. Convainquez-vous et tous les autres de l’importance (et des propriétés qui font gagner du temps ) des tests, ou ne vous en faites pas.

La plupart des réponses ici semblent concerner les meilleures pratiques de tests unitaires en général (quand, où, pourquoi et quoi), plutôt que d’écrire les tests eux-mêmes (comment). Étant donné que la question semblait assez spécifique sur la partie “comment”, je pensais que je posterais ceci, tiré d’une présentation “sac brun” que j’ai effectuée dans mon entreprise.

Les 5 lois de rédaction de Womp:


1. Utilisez des noms de méthodes de test longs et descriptifs.

- Map_DefaultConstructorShouldCreateEmptyGisMap() - ShouldAlwaysDelegateXMLCorrectlyToTheCustomHandlers() - Dog_Object_Should_Eat_Homework_Object_When_Hungry() 

2. Écrivez vos tests dans un style Arrangement / Act / Assert .

  • Bien que cette stratégie organisationnelle existe depuis un certain temps et ait été appelée à plusieurs resockets, l’introduction de l’acronyme «AAA» a été un excellent moyen de faire passer le message. Rendre tous vos tests compatibles avec le style AAA les rend faciles à lire et à maintenir.

3. Fournissez toujours un message d’échec avec vos Assert.

 Assert.That(x == 2 && y == 2, "An incorrect number of begin/end element processing events was raised by the XElementSerializer"); 
  • Une pratique simple mais gratifiante qui rend évident dans votre application coureur ce qui a échoué. Si vous ne fournissez pas de message, vous obtiendrez généralement quelque chose comme “vrai attendu, faux” dans votre sortie d’échec, ce qui vous oblige à lire le test pour savoir ce qui ne va pas.

4. Commentez la raison du test – quelle est l’hypothèse métier?

  /// A layer cannot be constructed with a null gisLayer, as every function /// in the Layer class assumes that a valid gisLayer is present. [Test] public void ShouldNotAllowConstructionWithANullGisLayer() { } 
  • Cela peut sembler évident, mais cette pratique protégera l’intégrité de vos tests de personnes qui ne comprennent pas la raison d’être du test. J’ai vu de nombreux tests supprimés ou modifiés parfaitement, simplement parce que la personne ne comprenait pas les hypothèses de vérification du test.
  • Si le test est sortingvial ou si le nom de la méthode est suffisamment descriptif, il peut être permis de laisser le commentaire désactivé.

5. Chaque test doit toujours annuler l’état de toute ressource qu’il touche

  • Utilisez des simulacres si possible pour éviter de traiter avec des ressources réelles.
  • Le nettoyage doit être effectué au niveau du test. Les tests ne doivent pas dépendre de l’ordre d’exécution.

Gardez ces objectives à l’esprit (adapté du livre xUnit Test Patterns by Meszaros)

  • Les tests devraient réduire le risque, pas l’introduire.
  • Les tests doivent être faciles à exécuter.
  • Les tests doivent être faciles à gérer au fur et à mesure que le système évolue autour d’eux

Certaines choses pour faciliter cela:

  • Les tests ne devraient échouer que pour une raison.
  • Les tests ne doivent tester qu’une seule chose
  • Minimiser les dépendances de test (pas de dépendances sur les bases de données, les fichiers, l’interface utilisateur, etc.)

N’oubliez pas que vous pouvez également effectuer des tests d’intégration avec votre framework xUnit, mais en séparant les tests d’intégration et les tests unitaires

Les tests doivent être isolés. Un test ne doit pas dépendre d’un autre. De plus, un test ne devrait pas dépendre de systèmes externes. En d’autres termes, testez votre code et non le code dont dépend votre code. Vous pouvez tester ces interactions dans le cadre de vos tests d’intégration ou fonctionnels.

Quelques propriétés de grands tests unitaires:

  • Lorsqu’un test échoue, il devrait être immédiatement évident où se trouve le problème. Si vous devez utiliser le débogueur pour identifier le problème, vos tests ne sont pas suffisamment précis. Avoir exactement une assertion par test aide ici.

  • Lorsque vous refactorez, aucun test ne doit échouer.

  • Les tests doivent être si rapides que vous n’hésitez jamais à les exécuter.

  • Tous les tests doivent toujours passer; aucun résultat non déterministe.

  • Les tests unitaires doivent être bien pris en compte, tout comme votre code de production.

@Alotor: Si vous suggérez qu’une bibliothèque ne devrait avoir que des tests unitaires sur son API externe, je ne suis pas d’accord. Je veux des tests unitaires pour chaque classe, y compris des classes que je n’expose pas aux appelants externes. (Cependant, si je ressens le besoin d’écrire des tests pour des méthodes privées, alors je dois refactoriser. )


EDIT: Il y avait un commentaire sur la duplication causée par “une assertion par test”. Plus précisément, si vous avez du code pour configurer un scénario, puis souhaitez en faire plusieurs, mais ne disposez que d’une seule assertion par test, vous pouvez dupliquer la configuration sur plusieurs tests.

Je ne prends pas cette approche. Au lieu de cela, j’utilise des assemblys de test par scénario . Voici un exemple approximatif:

 [TestFixture] public class StackTests { [TestFixture] public class EmptyTests { Stack _stack; [TestSetup] public void TestSetup() { _stack = new Stack(); } [TestMethod] [ExpectedException (typeof(Exception))] public void PopFails() { _stack.Pop(); } [TestMethod] public void IsEmpty() { Assert(_stack.IsEmpty()); } } [TestFixture] public class PushedOneTests { Stack _stack; [TestSetup] public void TestSetup() { _stack = new Stack(); _stack.Push(7); } // Tests for one item on the stack... } } 

Vous recherchez ensuite les comportements de la classe testée.

  1. Vérification des comportements attendus.
  2. Vérification des cas d’erreur.
  3. Couverture de tous les chemins de code dans la classe.
  4. Exercer toutes les fonctions membres dans la classe.

L’intention de base est d’augmenter votre confiance dans le comportement de la classe.

Ceci est particulièrement utile lorsque vous envisagez de refactoriser votre code. Martin Fowler a publié un article intéressant sur les tests sur son site Web.

HTH.

à votre santé,

Rob

Le test doit initialement échouer. Ensuite, vous devez écrire le code qui les fait passer, sinon vous risquez d’écrire un test qui est buggé et passe toujours.

J’aime l’acronyme Right BICEP du livre Pragmatic Unit Testing susmentionné:

  • Droite : Les résultats sont-ils exacts ?
  • B : Est-ce que toutes les conditions de base sont correctes?
  • I : Pouvons-nous vérifier les relations inverses?
  • C : Peut-on vérifier les résultats par d’autres moyens?
  • E : Peut-on forcer les conditions d’urgence à se produire?
  • P : Les caractéristiques de performance sont-elles dans les limites?

Personnellement, je pense que vous pouvez aller assez loin en vérifiant que vous obtenez les bons résultats (1 + 1 devrait renvoyer 2 dans une fonction d’addition), en essayant toutes les conditions aux limites auxquelles vous pouvez penser (par exemple, en utilisant deux nombres dont la sum est supérieur à la valeur maximale de l’entier dans la fonction d’ajout) et forçant des conditions d’erreur telles que des défaillances du réseau.

Les bons tests doivent être maintenables.

Je n’ai pas tout à fait compris comment faire cela dans des environnements complexes.

Tous les manuels commencent à se décoller lorsque votre base de code commence à atteindre des centaines de milliers ou des millions de lignes de code.

  • Les interactions d’équipe explosent
  • nombre de cas de test exploser
  • les interactions entre les composants explose.
  • le temps de construire tous les désintégrations devient une partie importante du temps de construction
  • un changement d’API peut générer des centaines de cas de test. Même si le changement de code de production était facile.
  • Le nombre d’événements requirejs pour séquencer les processus dans le bon état augmente, ce qui augmente le temps d’exécution du test.

Une bonne architecture peut contrôler une partie de l’explosion des interactions, mais inévitablement, à mesure que les systèmes deviennent plus complexes, le système de test automatisé se développe avec lui.

C’est là que vous devez commencer à gérer les compromis:

  • testez uniquement l’API externe, sinon le refactoring interne entraîne un remaniement significatif du scénario de test.
  • la configuration et le déassembly de chaque test deviennent plus compliqués car un sous-système encapsulé conserve plus d’état.
  • la compilation nocturne et l’exécution automatique des tests augmentent à des heures.
  • temps de compilation et d’exécution accrus signifie que les concepteurs ne font pas ou ne vont pas exécuter tous les tests
  • pour réduire les temps d’exécution des tests, vous considérez que les tests de séquençage réduisent l’installation et le déassembly

Vous devez également décider:

Où stockez-vous les cas de test dans votre base de code?

  • Comment documentez-vous vos cas de test?
  • les appareils de test peuvent-ils être réutilisés pour économiser la maintenance des tests?
  • Que se passe-t-il quand l’exécution d’un test de nuit échoue? Qui fait le sortingage?
  • Comment entretenez-vous les objects simulés? Si vous avez 20 modules qui utilisent tous leur propre version d’une API de journalisation simulée, modifiez rapidement les ondulations de l’API. Non seulement les cas de test changent, mais les 20 objects simulés changent. Ces 20 modules ont été écrits sur plusieurs années par de nombreuses équipes différentes. C’est un problème de réutilisation classique.
  • les individus et leurs équipes comprennent la valeur des tests automatisés dont ils n’aiment pas la façon dont les autres équipes le font. 🙂

Je pourrais continuer pour toujours, mais mon point est que:

Les tests doivent être maintenables.

J’ai déjà abordé ces principes dans Cet article de MSDN Magazine, que je pense important de lire pour tout développeur.

La façon dont je définis les “bons” tests unitaires est de savoir s’ils possèdent les trois propriétés suivantes:

  • Ils sont lisibles (nommage, assertions, variables, longueur, complexité ..)
  • Ils sont maintenables (pas de logique, pas trop spécifiés, basés sur l’état, refactorisés ..)
  • Ils sont dignes de confiance (testez la bonne chose, isolez, pas les tests d’intégration ..)
  • Les tests unitaires testent simplement l’API externe de votre unité, vous ne devez pas tester le comportement interne.
  • Chaque test de TestCase doit tester une (et une seule) méthode dans cette API.
    • Des cas de test supplémentaires doivent être inclus pour les cas d’échec.
  • Testez la couverture de vos tests: une fois qu’une unité est testée, les 100% des lignes à l’intérieur de cette unité devraient avoir été exécutées.

Jay Fields a beaucoup de bons conseils pour écrire des tests unitaires et il y a un post où il résume les conseils les plus importants . Vous y lirez que vous devriez penser de manière critique à votre contexte et juger si le conseil vaut pour vous. Vous obtenez une tonne de réponses étonnantes ici, mais c’est à vous de décider lequel convient le mieux à votre contexte. Essayez-les et refactorisez-les si cela vous semble mauvais.

Sincères amitiés

Ne supposez jamais qu’une méthode sortingviale à 2 lignes fonctionnera. Ecrire un test unitaire rapide est le seul moyen d’éviter que le test NULL manquant, le signe moins placé et / ou une erreur de scope subtile ne vous mordent, inévitablement lorsque vous avez encore moins de temps pour vous en occuper.

J’appuie la réponse “A TRIP”, sauf que les tests DEVRAIENT s’appuyer l’un sur l’autre !!!

Pourquoi?

DRY – Dont Repeat Yourself – s’applique également aux tests! Les dépendances de test peuvent aider à 1) économiser du temps d’installation, 2) économiser des ressources de matériel et 3) identifier les pannes. Bien sûr, uniquement si votre infrastructure de test prend en charge les dépendances de première classe. Sinon, je l’avoue, ils sont mauvais.

Suivi http://www.iam.unibe.ch/~scg/Research/JExample/

Les tests unitaires sont souvent basés sur des objects simulés ou des données simulées. J’aime écrire trois types de tests unitaires:

  • tests unitaires “transitoires”: ils créent leurs propres objects / données simulés et testent leur fonction avec, mais détruisent tout et ne laissent aucune trace (comme aucune donnée dans une firebase database de test)
  • Test unitaire “persistant”: ils testent des fonctions dans votre code en créant des objects / données qui seront nécessaires à une fonction plus avancée ultérieurement pour leur propre test unitaire (en évitant que ces fonctions avancées ne recréent à chaque fois leur propre ensemble d’objects / données simulés)
  • Tests unitaires “persistants”: tests unitaires utilisant des objects / données fictifs déjà présents (car créés dans une autre session de test unitaire) par les tests unitaires persistants.

Le but est d’éviter de tout rejouer pour pouvoir tester toutes les fonctions.

  • Je cours le troisième type très souvent car tous les objects / données simulés sont déjà présents.
  • Je cours le deuxième type chaque fois que mon modèle change.
  • Je lance le premier pour vérifier les fonctions de base de temps en temps, pour vérifier les régressions de base.

Pensez aux 2 types de tests et traitez-les différemment – tests fonctionnels et tests de performance.

Utilisez différentes entrées et mésortingques pour chacune. Vous devrez peut-être utiliser un logiciel différent pour chaque type de test.

J’utilise une convention de dénomination de test cohérente décrite par les normes de dénomination des tests unitaires de Roy Osherove. Chaque méthode d’une classe de test élémentaire donnée a le style de dénomination suivant MethodUnderTest_Scenario_ExpectedResult.

    La première section de nom de test est le nom de la méthode dans le système testé.
    Ensuite, le scénario spécifique testé.
    Enfin, voici les résultats de ce scénario.

Chaque section utilise Upper Camel Case et est délimitée par un sous score.

J’ai trouvé cela utile lorsque je lance le test, le test est groupé par le nom de la méthode testée. Et avoir une convention permet aux autres développeurs de comprendre l’intention du test.

J’ajoute également des parameters au nom de la méthode si la méthode testée a été surchargée.