Erreurs courantes lors de l'écriture des tests unitaires. Conférence Yandex

Si vous maîtrisez une petite liste d'erreurs typiques qui se produisent lors de l'écriture de tests unitaires, vous pouvez même aimer les écrire. Aujourd'hui, le chef de l'équipe de développement Yandex.Browser pour Android Konstantin kzaikin Zaikin partagera son expérience avec les lecteurs Habr.


- J'ai un rapport pratique. J'espère que cela vous sera bénéfique à tous - ceux qui sont déjà en train de passer des tests unitaires, ceux qui pensent à écrire, ceux qui essaient et qui n'ont pas réussi.

Nous avons un assez gros projet. L'un des plus grands projets mobiles en Russie. Nous avons beaucoup de code, beaucoup de tests. Les tests sont poursuivis à chaque demande de pool, ils ne tombent pas en même temps.

Qui sait quelle couverture de test il a dans le projet? Zéro, d'accord. Qui a des tests unitaires dans le projet? Et qui pense que les tests unitaires ne sont pas nécessaires? Je ne vois rien de mal à cela, il y a des gens qui en sont sincèrement convaincus, et mon histoire devrait les aider à en être convaincus.

Heureusement, des milliers de tests écologiques - nous ne sommes pas venus tout de suite. Il n'y a pas de solution miracle, et l'idée principale de mon rapport à l'écran:



Le dicton chinois est écrit en hiéroglyphes selon lequel un voyage d'un millier de personnes commence par un pas. Il semble qu'il y ait un tel analogue de ce dicton.

Il y a longtemps, nous avons décidé que nous devions améliorer notre produit, notre code, et nous nous dirigeons résolument vers cela. Sur ce chemin, nous avons rencontré beaucoup de bosses, un râteau sous-marin et rassemblé avec cela quelques croyances.



Pourquoi avons-nous besoin de tests?

Pour que les anciennes fonctionnalités ne tombent pas lorsque nous en introduisons de nouvelles. Pour avoir un badge sur GitHub. Pour refactoriser les fonctionnalités existantes - une réflexion approfondie, elle doit être révélée à ceux qui n'écrivent pas de tests. Afin que les fonctionnalités existantes ne tombent pas lors du refactoring, nous nous protégerons par des tests. Pour que le patron envoie une demande de pool, oui.

Mon avis - veuillez ne pas l'associer à l'avis de mon équipe - que les tests nous aident. Ils vous permettent d'exécuter votre code sans le mettre en production, sans l'installer sur des appareils, vous le lancez et l'exécutez très rapidement. Vous pouvez fuir tous les cas d'angle que vous n'avez pas dans la vie sur l'appareil et en production, et votre testeur ne les proposera pas. Mais en tant que développeur, vous les inventerez, les vérifierez et corrigerez les bogues à un stade précoce.

Très important: les tests indiquent comment, selon le développeur, le code devrait fonctionner et ce que, selon le développeur, vos méthodes devraient faire. Ce ne sont pas des commentaires qui éloignent et après un certain temps des commentaires utiles deviennent nuisibles. Il arrive que dans les commentaires, une chose soit écrite et que le code soit complètement différent. Les tests unitaires dans ce sens ne peuvent pas mentir. Si le test est vert, il documente ce qui s'y passe. Le test a échoué - vous avez violé l'intention principale du développeur.

Engagez des contrats. Ce ne sont pas des contrats signés et tamponnés, mais des contrats logiciels pour le comportement de classe. Si vous refactorisez, dans ce cas les contrats seront violés et les tests tomberont si vous les cassez. Si les contrats sont sauvegardés, les tests resteront verts, vous aurez plus confiance que votre refactoring est correct.



C'est l'idée générale de tout mon rapport. Vous pouvez montrer la première ligne et partir.

Beaucoup de gens pensent que le code de test est un code moyen, il n'est pas destiné à la production, vous pouvez donc l'écrire tel quel. Je suis fortement en désaccord avec cela et je pense que les tests doivent d'abord être abordés de manière responsable, ainsi que le code de production. Si vous les abordez de la même manière, les tests vous seront bénéfiques. Sinon, ce sera un charbon.

Plus précisément, les deux lignes ci-dessous font référence à n'importe quel code, semble-t-il.

KISS - restez simple, stupide. Pas besoin de compliquer. Les tests doivent être simples. Et le code de production doit être simple, mais les tests le sont surtout. Si vous avez des tests faciles à lire, alors ce seront des tests qui sont très probablement bien écrits, ils sont bien exprimés, ils seront faciles à tester. Même pendant la demande de pool, une personne qui regarde vos nouveaux tests comprendra ce que vous vouliez dire. Et si quelque chose se casse, vous pouvez facilement comprendre ce qui s'est passé.

SEC - ne vous répétez pas. Dans les tests, le développeur est souvent enclin à utiliser la technique interdite que personne ne semble utiliser dans la production - copier-coller. Dans la production d'un développeur qui copiera-collera activement, ils ne comprendront tout simplement pas. Dans les tests, c'est malheureusement une pratique normale. Pas besoin de le faire, car - la première ligne. Si vous écrivez les tests honnêtement, comme du vrai bon code, les tests vous seront utiles.

Pendant que nous développions nos centaines de milliers de lignes de code, écrivant des milliers de tests, collectant des râteaux, j'avais accumulé des commentaires typiques sur les tests. Je suis assez paresseux, et quand je suis allé aux demandes de pool et que j'ai observé les mêmes erreurs, basé sur le principe DRY, j'ai décidé d'écrire ces problèmes typiques, et je l'ai d'abord fait sur le Wiki interne puis j'ai posté des odeurs de test pratiques sur GitHub que vous pouvez suivre lorsque vous écrivez des tests.



Je vais énumérer par points. Incrémentez un compteur dans votre esprit si vous vous souvenez d'une telle odeur de test. Si vous comptez jusqu'à cinq, vous pouvez lever la main et crier "Bingo!" Et au final, je me demande qui a compté combien. Mon compteur sera égal au nombre de points, je les ai tous collectés moi-même.


Lien GitHub

La chose la plus difficile en programmation que vous connaissez. Et dans les tests, c'est vraiment important. Si vous ne nommez pas bien le test, vous ne pourrez probablement pas formuler ce que le test vérifie.

Les humains sont des créatures assez simples, ils sont facilement piégés dans les noms. Par conséquent, je vous demande de bien appeler les tests. Formulez un test pour vérifier et suivre des règles simples.

no_action_or_assertion


Si le nom du test ne contient pas de description de ce que le test vérifie, par exemple, vous avez la classe Controller et vous écrivez le test testController, que vérifiez-vous? Que devrait faire ce test? Très probablement, rien ou trop de choses à vérifier. Ni l'un ni l'autre ne nous conviennent. Par conséquent, il est nécessaire d'écrire au nom du test ce que nous vérifions.

long_name


Vous ne pouvez pas aller à l'autre extrême. Le nom du test doit être suffisamment court pour qu'une personne puisse facilement l'analyser. En ce sens, Kotlin est génial car il vous permet d'écrire des noms de test entre guillemets avec des espaces en anglais normal. Ils sont plus faciles à lire. Mais quand même, les noms longs sentent bon.

Si le nom de votre test est trop long, vous mettez probablement trop de méthodes de test dans une classe de test et vous devez clarifier ce que vous vérifiez. Dans ce cas, vous devez diviser votre classe de test en plusieurs. Pas besoin d'avoir peur de ça. Vous aurez un nom de classe de test qui vérifie le nom de votre code de production, et il y aura des noms de test courts.

old_prefix


C'est de l'atavisme. Auparavant, en Java, tout le monde testait en utilisant JUnit, où jusqu'à la quatrième version, il y avait un accord pour que les méthodes de test commencent par le mot test. Il en est ainsi, tout le monde l'appelle toujours ainsi. Mais il y a un problème, en anglais le mot test est le verbe «check». Les gens sont facilement pris dans ce piège et n'écrivent plus d'autres verbes. Écrivez testController. Il est facile de vérifier vous-même: si vous n’avez pas écrit un verbe ce que votre classe de test devrait faire, vous n’avez probablement pas vérifié quelque chose, vous ne l’avez pas assez bien écrit. Par conséquent, je vous demande toujours de supprimer le mot test des noms des méthodes de test.

Je dis des choses très simples, mais curieusement, elles aident. Si les tests sont bien appelés, très probablement sous le capot, ils auront l'air bien. C'est très simple.



J'ai en fait lu les identifiants des odeurs de test comme sur GitHub. Le lien est ci-dessous, vous pouvez marcher et utiliser.

multiple_asserts


Dans la méthode d'essai, il existe de nombreuses affirmations. Alors peut-être ou non? Peut-être. Est-ce bon ou mauvais? Je pense que c'est très mauvais. Si vous avez écrit plusieurs assertions dans une méthode de test, vous vérifiez plusieurs déclarations. Si vous testez votre test et que la première assertion tombe, le test atteindra-t-il la deuxième assertion? N'atteindra pas. Vous avez déjà après la chute de votre assemblage quelque part sur le CI que le test est tombé, allez réparer quelque chose, remplissez-le à nouveau, il tombera à l'assertion suivante. Cela pourrait très bien l'être.

Dans ce cas, ce serait beaucoup plus cool si vous sciez cette méthode de test en plusieurs, et toutes les méthodes avec plusieurs assertions tombaient en même temps, car elles seraient lancées indépendamment les unes des autres.

Quelques assertions supplémentaires peuvent masquer les différentes actions effectuées avec la classe de test. Je recommande d'écrire un test - une affirmation. Dans le même temps, les affirmations peuvent être assez complexes. Mon collègue, dans le tout premier rapport, a démontré un morceau de code où il a utilisé l'excellente construction assertThat et le matcher. J'adore vraiment les rencontres chez JUnit, vous pouvez donc l'utiliser aussi. Pour le lecteur de test, il ne s’agit que d’une courte déclaration. GitHub a des exemples de toutes ces odeurs et comment les corriger. Il y a un exemple de mauvais code et de bon code. Tout cela se fait sous la forme d'un projet que vous pouvez télécharger, ouvrir, compiler et exécuter tous les tests.

many_tests_in_one


L'odeur suivante est étroitement liée à la précédente. Vous faites quelque chose avec le système - vous faites une affirmation. Faire autre chose avec le système, de longues opérations - faire une assertion - faire autre chose. En fait, vous avez simplement vu plusieurs méthodes et vous obtenez de bonnes méthodes de test solides.

repeating_setup


Cela fait référence à la verbosité. Si vous avez une classe de test et que chaque méthode de test exécute les mêmes méthodes au début.

Une classe de test dans laquelle les mêmes méthodes sont exécutées au début. Cela semble un peu, mais dans chaque méthode de test, ces ordures sont présentes. Et si elle est commune à toutes les méthodes de test, pourquoi ne pas la faire glisser dans le constructeur ou le bloc Avant ou Avant chaque bloc dans JUnit 5. Si vous faites cela, la lisibilité de chaque méthode s'améliorera, et vous vous débarrasserez du péché SEC. Ces tests sont plus faciles à maintenir et à lire.



La fiabilité des tests est très importante. Il y a des signes par lesquels il peut être déterminé que le test va pleurer, être vert ou rouge. Lorsque le développeur l'écrit, il est sûr qu'il est vert, puis pour une raison quelconque, les tests deviennent verts ou rouges, ce qui nous donne la douleur et l'incertitude en général que les tests sont utiles. Nous ne sommes pas sûrs des tests, ce qui signifie que nous ne sommes pas sûrs qu'ils soient utiles.

aléatoire


J'ai moi-même écrit une fois des tests contenant Math.random (), fait des nombres aléatoires, fait quelque chose avec eux. Pas besoin de faire ça. Nous nous attendons à ce que le système de test entre dans le système de test dans la même configuration, et la sortie de celui-ci doit également être la même. Par conséquent, dans les tests unitaires, par exemple, vous n'avez jamais besoin d'effectuer d'opérations avec le réseau. Parce que le serveur peut ne pas répondre, il peut y avoir différents horaires, autre chose.

Si vous avez besoin d'un test qui fonctionne avec le réseau, faites un proxy, local, n'importe quoi, mais en aucun cas allez sur un vrai réseau. C'est le même hasard. Et bien sûr, vous ne pouvez pas utiliser de données aléatoires. Si vous devez faire quelque chose, faites quelques exemples avec des conditions aux limites, avec de mauvaises conditions, mais elles devraient être codées en dur.

tread_sleep


Un problème classique auquel les développeurs sont confrontés lorsqu'ils essaient de tester une sorte de code asynchrone. C'est que j'ai fait quelque chose dans le test, puis je dois attendre jusqu'à ce qu'il soit terminé. Comment faire? Thread.sleep (), bien sûr.

Il y a un problème. Lorsque vous avez développé votre test, par exemple, vous l'avez fait sur une partie de votre machine à écrire, cela fonctionne à une certaine vitesse. Vous exécutez les tests sur une autre machine. Et que se passera-t-il si votre système ne parvient pas à fonctionner pendant le temps Thread.sleep ()? Le test devient rouge. C'est inattendu. Par conséquent, la recommandation ici est, si vous effectuez des opérations asynchrones, ne les testez pas du tout. Presque n'importe quelle opération asynchrone peut être déployée de sorte que vous ayez une sorte de mécanisme conditionnel qui fournit un fonctionnement asynchrone et un bloc de code exécuté de manière synchrone. Par exemple, AsyncTask à l'intérieur a un bloc de code exécuté de manière synchrone. Vous pouvez facilement le tester de manière synchrone, sans asynchronisme. Il n'est pas nécessaire de tester AsyncTask lui-même, c'est une classe framework, pourquoi la tester? Accrochez-vous et votre vie sera plus facile.

Thread.sleep () est très douloureux. En plus du fait qu'elle aggrave la fiabilité des tests, car elle leur permet de pleurer en raison de différents timing sur les appareils, elle ralentit également l'exécution de vos tests. Qui aimerait que ses tests unitaires, qui devraient être exécutés en millisecondes, se déroulent pendant cinq secondes, car j'ai défini le sommeil de la bande de roulement?

modify_global


L'odeur typique est que nous avons modifié une variable statique globale au début du test pour vérifier que notre système fonctionne correctement, mais n'est pas revenu à la fin. Ensuite, nous obtenons une situation intéressante: sur la machine, le développeur a exécuté les tests dans une séquence, a d'abord vérifié la variable globale avec la valeur par défaut, puis l'a modifiée dans un autre test, puis a fait autre chose. Les deux tests sont verts. Et sur CI, c'est arrivé, les tests ont commencé dans l'ordre inverse. Et l'un ou les deux tests seront rouges, bien qu'ils soient tous verts.

Vous devez nettoyer après vous. Règles de reconnaissance dans ce sens: modification de la variable globale - retour à l'état d'origine. Mieux encore, assurez-vous que les états mondiaux ne sont pas utilisés. Mais c'est une pensée plus profonde. Il s'agit du fait que les tests mettent parfois en évidence des défauts d'architecture. Si nous devons changer les états globaux et les remettre à leur état d'origine afin d'écrire des tests, sommes-nous tous bien dans notre architecture? Avons-nous vraiment besoin de variables globales, par exemple? En règle générale, vous pouvez vous en passer en injectant certaines classes de contextes ou quelque chose, afin de pouvoir les réinitialiser, les injecter et les réinitialiser à chaque fois dans le test.

@VisibleForTesting


Testez l'odeur pour avancé. La nécessité d'utiliser une telle chose ne se pose pas le premier jour, en règle générale. Vous avez déjà testé quelque chose, puis vous avez dû traduire la classe dans un état spécifique. Et vous vous faites une porte dérobée. Vous avez une classe de production, et vous créez une méthode spécifique qui ne sera jamais appelée en production, et à travers elle vous injectez quelque chose dans la classe ou changez son état. Ainsi, brisant malicieusement l'encapsulation. En production, votre classe fonctionne d'une manière ou d'une autre, mais dans les tests, en fait, c'est une classe différente, vous communiquez avec elle via d'autres entrées et sorties. Et ici, vous pouvez obtenir une situation où vous modifiez la production, mais les tests ne le remarquent pas. Les tests continuent de passer par la porte dérobée et n'ont pas remarqué que, par exemple, des exceptions ont commencé à tirer dans le constructeur, car elles passent par un autre constructeur.

En général, vous devez tester vos classes via les mêmes entrées et sorties qu'en production. Il ne devrait y avoir accès à aucune méthode uniquement pour les tests.



Combien de nos 15 mille tests sont effectués? Environ 20 minutes, à chaque demande de pool, sur Team City, les développeurs sont obligés d'attendre. Tout simplement parce que 15 000, c'est beaucoup de tests. Et dans cette section, j'ai compilé des odeurs qui ralentissent les tests. Bien que thread_sleep était déjà là.

inutile_android_test


Android a des tests d'instrumentation, ils sont beaux, ils fonctionnent sur un appareil ou un émulateur. Cela soulèvera votre projet complètement, pour de vrai, mais ils sont très lents. Et pour eux, vous devez même élever un émulateur entier. Même si vous imaginez que vous avez un émulateur surélevé sur CI - il coïncide tellement que vous en avez un - alors l'exécution du test sur l'émulateur prendra beaucoup plus de temps que sur la machine hôte, par exemple, en utilisant Robolectric. Bien qu'il existe d'autres méthodes. C'est un tel framework qui vous permet de travailler avec des classes du framework Android sur la machine hôte, en pur Java. Nous l'utilisons assez activement. Auparavant, Google était plutôt cool à ce sujet, mais maintenant les googleurs eux-mêmes en parlent dans divers rapports, il est recommandé de l'utiliser.

inutile_robolectrique


Le framework Android de Robolectric est émulé. Elle n'est pas complète là-bas, bien que la mise en œuvre soit la plus éloignée, la plus complète. Il est presque réel Android, ne fonctionnant que sur votre bureau, ordinateur portable ou CI. Mais il n'a pas non plus besoin d'être utilisé partout. Robolectric n'est pas gratuit. Si vous avez un test que vous avez héroïquement transféré de l'instrumentation Android vers Robolectric, vous devez penser - peut-être aller encore plus loin, se débarrasser de Robolectric, le transformer en le test JUnit le plus simple? Les tests robotiques prennent du temps pour s'initialiser, essayer de charger des ressources, initialiser votre activité, votre application et tout le reste. Cela prend du temps. Ce n'est pas une seconde, c'est des millisecondes, parfois des dizaines et des centaines. Mais quand il y a beaucoup de tests, même cela compte.

Il existe des techniques qui se débarrassent de Robolectric. Vous pouvez isoler votre code via des interfaces en enveloppant la partie plate-forme entière avec des interfaces. Ensuite, il y aura juste un test d'hôte JUnit. JUnit sur la machine hôte est très rapide, il y a une quantité minimale de surcharge, ces tests peuvent être exécutés par milliers et dizaines de milliers, ils s'exécuteront en une minute, quelques minutes. Malheureusement, nos tests prennent beaucoup de temps car nous avons beaucoup de tests d'instrumentation Android, car nous avons une partie native dans le navigateur et nous sommes obligés de les exécuter sur un véritable émulateur ou appareil. Pourquoi si longtemps.

Je ne t'ennuierai plus. Combien d'odeurs avez-vous? Jusqu'à présent, sept au maximum. Abonnez-vous à la chaîne , mettez les étoiles.

Source: https://habr.com/ru/post/fr436850/


All Articles