Une stratégie efficace pour les tests de code automatisés est extrêmement importante pour assurer le travail rapide et de haute qualité des équipes de programmeurs impliqués dans le support et le développement de projets Web. L'auteur de l'article dit que dans l'entreprise
StackPath , dans laquelle il travaille, maintenant tout fonctionne
bien avec les tests. Ils ont de nombreux outils pour vérifier le code. Mais à partir d'une telle variété, vous devez choisir ce qui convient le mieux dans chaque cas. Il s'agit d'un problème distinct. Et une fois les outils nécessaires sélectionnés, vous devez toujours prendre une décision sur l'ordre de leur utilisation.

L'auteur de l'article dit que StackPath est satisfait du niveau de confiance dans la qualité du code qui a été atteint grâce au système de test appliqué. Ici, il souhaite partager une description des principes de test développés par l'entreprise et parler des outils utilisés.
Principes de test
Avant de parler d'outils spécifiques, il convient de réfléchir à la réponse à la question de savoir quels sont les bons tests. Avant de commencer à travailler sur notre
portail pour les clients, nous avons formulé et écrit les principes que nous aimerions suivre lors de la création de tests. Ce que nous avons fait en premier lieu est exactement ce qui nous a aidés dans le choix des outils.
Voici les quatre principes en question.
▍ Principe numéro 1. Les tests doivent être compris comme des tâches d'optimisation
Une stratégie de test efficace consiste à résoudre le problème de maximiser une certaine valeur (dans ce cas, le niveau de confiance que l'application fonctionnera correctement) et de minimiser certains coûts (ici les "coûts" sont représentés par le temps nécessaire pour prendre en charge et exécuter les tests). Lors de la rédaction des tests, nous posons souvent les questions suivantes liées au principe décrit ci-dessus:
- Quelle est la probabilité que ce test trouve une erreur?
- Ce test améliore-t-il notre système de test et les coûts des ressources nécessaires pour l'écrire valent-ils les avantages qui en découlent?
- Est-il possible d'obtenir le même niveau de confiance dans l'entité testée que ce test donne en créant un autre test plus facile à écrire, à maintenir et à exécuter?
▍ Principe n ° 2. La surutilisation de mox doit être évitée.
L'une de mes explications préférées du terme «mok» a été donnée dans
cette présentation de la conférence Assert.js 2018. L'orateur a ouvert la question plus profondément que je ne vais l'ouvrir ici. Dans le discours, la création de mokas est comparée à des «trous de poing en réalité». Et je pense que c'est une façon très visuelle de percevoir les moks. Bien qu'il y ait des mokas dans nos tests, nous comparons la diminution du «coût» des tests que les mokas fournissent en raison de la simplification du processus d'écriture et d'exécution des tests, avec la diminution de la valeur des tests qui fait qu'un autre trou est fait dans la réalité.
Auparavant, nos programmeurs s'appuyaient fortement sur des tests unitaires écrits afin que toutes les dépendances enfants soient remplacées par des mokas à l'aide de l'API de rendu
enzymatique peu profond. Les entités rendues de cette manière ont ensuite été vérifiées à l'aide d'instantanés
Jest . Tous ces tests ont été écrits en utilisant un modèle similaire:
it('renders ', () => { const wrapper = shallow();
Ces tests sont remplis de réalité dans de nombreux endroits. Cette approche permet d'obtenir très facilement une couverture de code à 100% avec des tests. Lorsque vous écrivez de tels tests, vous devez penser très peu, mais si vous ne vérifiez pas tous les nombreux points d'intégration, ces tests ne sont pas particulièrement utiles. Tous les tests peuvent réussir, mais cela ne donne pas beaucoup de confiance dans le fonctionnement de l'application. Et pire encore, tous les mokas ont un «prix» caché que vous devez payer une fois les tests écrits.
▍ Principe n ° 3. Les tests devraient faciliter la refactorisation du code, pas la compliquer.
Des tests comme celui montré ci-dessus compliquent la refactorisation. Si je trouve que dans de nombreux endroits du projet, il y a du code en double, et après un certain temps je formate ce code en tant que composant séparé, alors tous les tests pour les composants dans lesquels j'utiliserai ce nouveau composant échoueront. Les composants dérivés à l'aide de la technique de rendu peu profond sont déjà autre chose. Là où j'avais l'habitude de répéter le balisage, il y a maintenant un nouveau composant.
Une refactorisation plus complexe, qui implique l'ajout de certains composants à un projet et la suppression de plusieurs composants, crée encore plus de confusion. Le fait est que vous devez ajouter de nouveaux tests au système et en supprimer les tests inutiles. La régénération d'instantanés est une tâche simple, mais quelle est la valeur de ces tests? Même s'ils sont capables de trouver une erreur, il vaudrait mieux qu'ils l'aient manqué dans une série de changements de snapshots et simplement vérifié de nouveaux snapshots sans y passer trop de temps.
Par conséquent, ces tests ne permettent pas particulièrement de refactoriser. Idéalement, aucun test ne devrait échouer si j'effectue une refactorisation, après quoi ce que l'utilisateur voit et ce avec quoi il interagit n'a pas changé. Et vice versa - si je modifie ce que l'utilisateur contacte, au moins un test devrait échouer. Si les tests suivent ces deux règles, ils sont un excellent outil pour garantir que quelque chose que les utilisateurs rencontrent ne change pas pendant la refactorisation.
▍ Principe n ° 4. Les tests doivent reproduire le fonctionnement des utilisateurs réels avec l'application
J'aimerais que les tests échouent uniquement si quelque chose a changé avec lequel l'utilisateur interagit. Cela signifie que les tests doivent fonctionner avec l'application de la même manière que les utilisateurs travaillent avec elle. Par exemple, un test doit vraiment interagir avec les éléments du formulaire et, comme un utilisateur, doit saisir du texte dans les champs de saisie de texte. Les tests ne doivent pas accéder aux composants et appeler indépendamment les méthodes de leur cycle de vie, ne doivent pas écrire quelque chose dans l'état des composants, ou faire quelque chose qui repose sur les subtilités de l'implémentation des composants. Étant donné que, finalement, je souhaite vérifier la partie du système qui est en contact avec l'utilisateur, il est logique de s'efforcer de faire en sorte que les tests, lorsqu'ils interagissent avec le système, reproduisent les actions des utilisateurs réels aussi près que possible.
Outils de test
Maintenant que nous avons défini les objectifs que nous voulons atteindre, parlons des outils que nous avons choisis pour cela.
▍TypeScript
Notre base de code utilise TypeScript. Nos services backend sont écrits en Go et interagissent les uns avec les autres à l'aide de gRPC. Cela nous permet de générer des clients gRPC typés pour une utilisation sur un serveur GraphQL. Les résolveurs de serveur GraphQL sont typés à l'aide de types générés à l'aide de
graphql-code-generator . Et enfin, nos requêtes, mutations, ainsi que les composants d'abonnement et les hooks sont entièrement typés. Une couverture complète de notre base de code avec des types élimine toute une classe d'erreurs causées par le fait que le formulaire de données n'est pas ce que le programmeur attend. La génération de types à partir des fichiers de schéma et de protobuf garantit que l'ensemble de notre système, dans toutes les parties de la pile de technologies utilisées, reste homogène.
EstJest (test unitaire)
Comme framework pour tester le code, nous utilisons
Jest et
@ testing-library / react . Dans les tests créés à l'aide de ces outils, nous testons des fonctions ou des composants indépendamment du reste du système. Nous testons généralement les fonctions et les composants qui sont le plus souvent utilisés dans une application, ou ceux qui ont de nombreuses façons d'exécuter du code. Ces chemins sont difficiles à vérifier pendant l'intégration ou les tests de bout en bout (E2E).
Les tests unitaires sont pour nous un moyen de tester de petites pièces. Les tests d'intégration et de bout en bout font un excellent travail de vérification du système à plus grande échelle, vous permettant de vérifier le niveau général d'intégrité de l'application. Mais parfois, vous devez vous assurer que les petits détails fonctionnent, et l'écriture de tests d'intégration pour toutes les utilisations possibles du code est trop coûteuse.
Par exemple, nous devons vérifier que la navigation au clavier fonctionne dans le composant responsable de l'utilisation de la liste déroulante. Mais en même temps, nous ne voudrions pas vérifier toutes les variantes possibles d'un tel comportement lors du test de l'application entière. Par conséquent, nous testons soigneusement la navigation de manière isolée et lorsque nous testons des pages à l'aide du composant approprié, nous ne prêtons attention qu'à la vérification des interactions de niveau supérieur.
Outils de test
▍Cypress (tests d'intégration)
Les tests d'intégration créés à l'aide de
Cypress sont au cœur de notre système de test. Lorsque nous avons commencé à créer le portail StackPath, ce sont les premiers tests que nous avons écrits, car ils sont très précieux avec très peu de frais généraux pour leur création. Cypress affiche l'intégralité de notre application dans un navigateur et exécute des scripts de test. L'ensemble de notre frontend fonctionne exactement de la même manière que lorsque les utilisateurs travaillent avec. Certes, la couche réseau du système est remplacée par mokami. Chaque requête réseau qui parviendrait normalement au serveur GraphQL renvoie des données conditionnelles à l'application.
L'utilisation de simulations pour simuler la couche réseau d'une application présente de nombreux atouts:
- Les tests sont plus rapides. Même si le backend du projet est extrêmement rapide, le temps nécessaire pour renvoyer les réponses aux requêtes faites pendant toute la suite de tests peut être assez important. Et si Moki est responsable du retour des réponses, les réponses sont retournées instantanément.
- Les tests deviennent de plus en plus fiables. L'une des difficultés pour effectuer des tests complets de bout en bout d'un projet est qu'il est nécessaire de prendre en compte l'état variable des données du réseau et du serveur, qui peut changer. Si l'accès réel au réseau est simulé à l'aide de moxas, cette variabilité disparaît.
- Il est facile de reproduire des situations qui nécessitent la répétition exacte de certaines conditions. Par exemple, dans un système réel, il sera difficile de faire échouer certaines requêtes de manière stable. Si vous devez vérifier la bonne réaction de l'application aux demandes infructueuses, alors moki vous permet facilement de reproduire les situations d'urgence.
Bien que le remplacement de l'ensemble du backend par mok semble une tâche ardue, toutes les données conditionnelles sont typées à l'aide des mêmes types TypeScript générés que ceux utilisés dans l'application. C'est-à-dire que ces données, au moins - en termes de structure, sont garanties d'être équivalentes à ce qu'un backend normal retournerait. Au cours de la plupart des tests, nous avons toléré les inconvénients de l'utilisation de mooks plutôt que de vrais appels de serveur.
De plus, les programmeurs sont très heureux de travailler avec Cypress. Les tests s'exécutent dans le Cypress Test Runner. Les descriptions des tests s'affichent à gauche et l'application de test s'exécute dans l'élément
iframe
principal. Après avoir commencé le test, vous pouvez étudier ses différentes étapes et découvrir comment l'application s'est comportée à un moment ou à un autre. Étant donné que l'outil d'exécution des tests s'exécute dans le navigateur lui-même, vous pouvez utiliser les outils du navigateur du développeur pour déboguer les tests.
Lors de l'écriture de tests frontaux, il arrive souvent que cela prenne beaucoup de temps pour comparer ce que fait le test avec l'état du DOM à un certain moment du test. Cypress simplifie considérablement cette tâche, car le développeur peut voir tout ce qui se passe avec l'application testée.
Voici un clip vidéo qui le démontre.
Ces tests illustrent parfaitement nos principes de test. Le rapport de leur valeur à leur "prix" nous convient. Les tests reproduisent de manière très similaire les actions de l'utilisateur réel interagissant avec l'application. Et seule la couche réseau du projet a été remplacée par mokami.
▍Cypress (test de bout en bout)
Nos tests E2E sont également écrits en utilisant Cypress, mais en eux nous n'utilisons pas moki ni pour simuler le niveau réseau d'un projet ni pour simuler quoi que ce soit d'autre. Lors des tests, l'application accède au vrai serveur GraphQL, qui fonctionne avec des instances réelles de services backend.
Les tests de bout en bout sont extrêmement précieux pour nous. Le fait est que ce sont les résultats de ces tests qui nous permettent de savoir si quelque chose fonctionne comme prévu ou ne fonctionne pas. Aucune maquette n'est utilisée lors de ces tests, par conséquent, l'application fonctionne exactement de la même manière que lorsqu'elle est utilisée par de vrais clients. Cependant, il convient de noter que les tests de bout en bout sont «plus chers» que les autres. Ils sont plus lents, plus difficiles à écrire, compte tenu de la possibilité d'échecs à court terme lors de leur mise en œuvre. Des travaux supplémentaires sont nécessaires pour garantir que le système reste dans un état connu avant d'exécuter les tests.
Les tests doivent généralement être exécutés à un moment où le système est dans un état connu. Une fois le test terminé, le système passe à un autre état connu. Dans le cas des tests d'intégration, il n'est pas difficile d'obtenir ce comportement du système, car les appels à l'API sont remplacés par des mokas, et, par conséquent, chaque test s'exécute dans des conditions prédéterminées contrôlées par le programmeur. Mais dans le cas des tests E2E, il est déjà plus difficile de le faire, car l'entrepôt de données du serveur contient des informations qui peuvent changer pendant le test. Par conséquent, le développeur doit trouver un moyen de s'assurer que lorsque le test commencera, le système sera dans un état précédemment connu.
Au début de l'exécution de test de bout en bout, nous exécutons un script qui, en effectuant des appels directs à l'API, crée un nouveau compte avec des piles, des sites, des charges de travail, des moniteurs, etc. Chaque session de test implique l'utilisation d'une nouvelle instance d'un tel compte, mais tout le reste de temps en temps reste inchangé. Le script, après avoir fait tout ce qui est nécessaire, forme un fichier contenant les données utilisées pour exécuter les tests (il contient généralement des informations sur les identificateurs d'instance et les domaines). En conséquence, il s'avère que le script vous permet de mettre le système dans un état précédemment connu avant d'exécuter les tests.
Étant donné que les tests de bout en bout sont «plus chers» que les autres types de tests, nous, par rapport aux tests d'intégration, écrivons moins de tests de bout en bout. Nous nous efforçons de nous assurer que les tests couvrent les fonctionnalités essentielles des applications. Par exemple, cela consiste à enregistrer des utilisateurs et leur connexion, à créer et à configurer un site / une charge de travail, etc. Grâce à des tests d'intégration approfondis, nous savons qu'en général, notre frontend est fonctionnel. Mais des tests de bout en bout ne sont nécessaires que pour s'assurer que lors de la connexion du frontend au backend, il ne se passe rien que les autres tests ne puissent pas détecter.
Inconvénients de notre stratégie de test complète
Bien que nous soyons très satisfaits des tests et de la stabilité de l'application, l'utilisation d'une stratégie de test complète comme la nôtre présente également des inconvénients.
Pour commencer, l'application d'une telle stratégie de test signifie que tous les membres de l'équipe doivent être familiers avec de nombreux outils de test, et pas seulement avec un. Tout le monde a besoin de connaître Jest, @ testing-library / react et Cypress. Mais en même temps, les développeurs n'ont pas seulement besoin de connaître ces outils. Ils doivent également être en mesure de décider dans quelle situation celle-ci doit être utilisée. Vaut-il la peine de tester une nouvelle opportunité d'écrire un test de bout en bout, ou le test d'intégration est-il suffisant? Est-il nécessaire, en plus du test de bout en bout ou d'intégration, d'écrire un test unitaire pour vérifier les petits détails de l'implémentation de cette nouvelle fonctionnalité?
Sans aucun doute, cela, pour ainsi dire, «charge la tête» de nos programmeurs, tout en utilisant le seul outil qu'ils ne subiraient pas une telle charge. Habituellement, nous commençons par des tests d'intégration, et après cela, si nous constatons que la fonctionnalité à l'étude est particulièrement importante et dépend fortement de la partie serveur du projet, nous ajoutons le test de bout en bout approprié. Ou nous commençons par des tests unitaires, en faisant cela si nous pensons qu'un test unitaire ne sera pas en mesure de vérifier toutes les subtilités de la mise en œuvre d'un certain mécanisme.
Bien sûr, nous sommes toujours confrontés à des situations où il n'est pas clair par où commencer. Mais, comme nous devons constamment prendre des décisions concernant les tests, certains modèles de situations courantes commencent à émerger. Par exemple, nous testons généralement les systèmes de validation des formulaires à l'aide de tests unitaires. Cela est dû au fait que pendant le test, vous devez vérifier de nombreux scénarios différents. Dans le même temps, tout le monde dans l'équipe le sait et ne perd pas de temps à planifier une stratégie de test lorsque l'un d'eux a besoin de tester le système de validation de formulaire.
Un autre inconvénient de l'approche que nous utilisons est la complication de la collecte de données sur la couverture du code par des tests. Bien que cela soit possible, c'est beaucoup plus compliqué que dans une situation où l'on est utilisé pour tester un projet. Bien que la recherche d'un nombre important de couvertures de code par des tests puisse entraîner une détérioration de la qualité des tests, ces informations sont précieuses pour trouver des «trous» dans la suite de tests utilisée. Le problème de l'utilisation de plusieurs outils de test est que, pour comprendre quelle partie du code n'a pas été testée, vous devez combiner les rapports sur la couverture du code avec les tests reçus de différents systèmes. C'est possible, mais c'est certainement beaucoup plus difficile que de lire un rapport généré par n'importe quel moyen de test.
Résumé
Lors de l'utilisation de nombreux outils de test, nous avons été confrontés à des tâches difficiles. Mais chacun de ces outils sert son propre objectif. Au final, nous pensons avoir fait ce qu'il fallait en les incluant dans notre système de test de code. Tests d'intégration - c'est là qu'il est préférable de commencer à créer un système de test au début du travail sur une nouvelle application ou lors de l'équipement des tests d'un projet existant. Il sera utile d'essayer d'ajouter des tests de bout en bout au projet le plus tôt possible, en vérifiant les caractéristiques les plus importantes du projet.
Lorsque la suite de tests contient des tests d'intégration et de bout en bout, cela devrait conduire au fait que le développeur recevra un certain niveau de confiance dans la santé de l'application lorsque des modifications y seront apportées. Si, au cours des travaux sur le projet, des erreurs ont commencé à apparaître qui ne sont pas détectées par les tests, il convient de déterminer quels tests pourraient détecter ces erreurs et si l'apparition d'erreurs indique des défauts dans l'ensemble du système de test utilisé dans le projet.
Bien sûr, nous ne sommes pas immédiatement arrivés à notre système de test actuel. De plus, nous nous attendons à ce que ce système, à mesure que notre projet se développe, se développe. Mais maintenant, nous aimons vraiment notre approche des tests.
Chers lecteurs! Quelles stratégies suivez-vous dans les tests frontaux? Quels outils de test frontaux utilisez-vous?
