Éléments à prendre en compte lors de l'écriture de tests E2E #frontend@twiliosendgrid

Publié: 2020-09-19

Chez Twilio SendGrid, nous écrivons des tests de bout en bout (E2E) vers la fin d'un nouveau cycle de développement de fonctionnalités ou de pages pour nous assurer que toutes les parties sont connectées et fonctionnent correctement entre le frontend et le backend du point de vue de l'utilisateur final.

Nous avons expérimenté divers frameworks et bibliothèques de test E2E tels que notre propre framework Ruby Selenium interne personnalisé, WebdriverIO, et principalement Cypress pendant plus de deux ans, comme indiqué dans la première et la deuxième partie de la série d'articles de blog documentant notre migration à travers tous les solutions. Quel que soit le framework ou la bibliothèque que nous avons utilisé, nous nous sommes retrouvés à poser les mêmes questions sur les fonctionnalités pour lesquelles nous pouvons automatiser et écrire des tests E2E. Après avoir identifié les fonctionnalités que nous pouvons tester, nous avons également remarqué que nous appliquions sans cesse la même stratégie générale pour l'écriture et la mise en place des tests.

Ce billet de blog ne nécessite aucune connaissance préalable de l'écriture de tests E2E dans une bibliothèque ou un framework donné, mais cela aide si vous avez vu des applications Web et vous êtes demandé comment automatiser au mieux les choses dans le navigateur pour tester le bon fonctionnement des pages. Notre objectif est de vous expliquer comment réfléchir aux tests E2E, afin que vous puissiez appliquer ces questions et la stratégie générale d'écriture de tests à n'importe quel cadre de votre choix.

Questions à poser si vous pouvez automatiser un test E2E

Lorsqu'il s'agit d'écrire des tests E2E, nous devons nous assurer que les flux dans les pages que nous testons dans notre application répondent à certains critères. Passons en revue certaines des questions de haut niveau que nous nous posons pour déterminer s'il est possible d'automatiser un test E2E.

1. Est-il possible de réinitialiser les données utilisateur à un certain état avant chaque test via un moyen fiable tel que l'API ? S'il n'existe aucun moyen de réinitialiser de manière fiable un utilisateur à l'état souhaité, il ne peut pas être automatisé et devrait s'exécuter dans le cadre de vos tests de blocage avant le déploiement. C'est aussi un anti-modèle et généralement non déterministe de ramener un utilisateur à un certain état via l'interface utilisateur, car il est lent et l'automatisation des étapes via l'interface utilisateur est déjà assez floue. Il est plus fiable d'effectuer des appels d'API pour réinitialiser l'état de l'utilisateur sans jamais avoir à ouvrir une page dans le navigateur. Une autre alternative, si vous avez un service qui existe, est de créer de nouveaux utilisateurs avant chaque test avec les bonnes données. Tant que nous réinitialisons un utilisateur persistant ou créons un utilisateur avant chaque test, nous pouvons alors nous concentrer sur les parties que nous testons sur la page.

2. Avons-nous le contrôle sur la fonctionnalité, l'API ou le système que nous avons l'intention de tester ? S'il s'agit d'un service tiers sur lequel vous comptez pour la facturation ou pour toute autre fonctionnalité, existe-t-il un moyen de les simuler ou de le faire fonctionner de manière déterministe avec certaines valeurs ? Vous voulez avoir le plus de contrôle possible sur le test pour réduire la desquamation. Vous pouvez créer des utilisateurs de test dédiés avec des ressources ou des données isolées par exécution de test afin qu'elles ne soient pas affectées par quoi que ce soit d'autre.

3. Le service ou la fonctionnalité lui-même est-il suffisamment cohérent pour fonctionner dans un délai raisonnable ? Souvent, vous devrez peut-être implémenter une interrogation ou attendre que certaines données soient traitées et transmises à la base de données (comme des mises à jour asynchrones plus lentes et des événements de courrier électronique déclenchés). Si ces services se produisent fréquemment dans un laps de temps raisonnable et fiable, vous pouvez définir des délais d'interrogation appropriés en attendant que des éléments DOM spécifiques s'affichent ou que des données soient mises à jour.

4. Pouvons-nous sélectionner les éléments avec lesquels nous devons interagir sur une page ? Avez-vous affaire à des iframes ou à des éléments générés sur lesquels vous n'avez aucun contrôle et que vous ne pouvez pas modifier ? Afin d'interagir avec les éléments d'une page, vous pouvez ajouter des sélecteurs plus spécifiques tels que les attributs `data-hook` ou `data-testid` plutôt que de sélectionner des identifiants ou des noms de classe. Les identifiants et les noms de classe sont plus susceptibles de changer car ils sont généralement associés à des styles. Imaginez essayer de sélectionner des noms de classe ou des identifiants hachés à partir de composants stylés ou de modules CSS autrement. Pour les éléments générés par des tiers ou les bibliothèques de composants open source comme react-select, vous pouvez envelopper ces éléments avec un élément parent avec un attribut `data-hook` et sélectionner les enfants en dessous. Pour traiter les iframes, nous avons créé des commandes personnalisées pour extraire les éléments DOM dont nous avons besoin pour affirmer et agir, sur lesquels nous fournirons un exemple plus tard.

Il y a d'autres considérations à prendre en compte, mais tout se résume à une seule question : pouvons-nous répéter ce test E2E de manière cohérente et opportune et obtenir les mêmes résultats ?

Stratégie générale pour écrire des tests E2E

1. Déterminez les cas de test de grande valeur que nous pouvons automatiser . Certains exemples incluent des tests de chemin heureux couvrant la majeure partie d'un flux de fonctionnalités : effectuer des opérations CRUD via l'interface utilisateur pour les informations de profil d'un utilisateur, filtrer une table pour faire correspondre les résultats en fonction de certaines données, créer une publication ou configurer une clé API. Cependant, il peut être préférable de couvrir d'autres cas extrêmes et la gestion des erreurs avec des tests unitaires et d'intégration. Passez en revue les questions que nous avons mentionnées dans la section précédente pour vous aider à raccourcir votre liste de cas de test.

2. Réfléchissez à la manière de répéter ces tests en configurant ou en supprimant l'API autant que possible. Pour les cas de test automatisables de grande valeur, commencez à noter les éléments que vous devez configurer via l'API. Certains exemples ensemencent l'utilisateur avec des données appropriées si l'utilisateur ne dispose pas de suffisamment de données filtrables pour la pagination, si les données de l'utilisateur expirent sur une fenêtre glissante de 30 jours, ou si nous devons éventuellement supprimer certaines données restantes de succès ou incomplètes tests avant que le test en cours ne recommence. Les tests doivent pouvoir s'exécuter et se configurer dans le même état reproductible, quelle que soit la réussite ou l'échec du dernier test.

Il est important de réfléchir : comment puis-je réinitialiser les données de cet utilisateur au point de départ afin de ne tester que la partie de la fonctionnalité que je souhaite ?

Par exemple, si vous souhaitez tester la possibilité pour un utilisateur d'ajouter une publication afin qu'elle apparaisse éventuellement dans la liste des publications de l'utilisateur, la publication doit d'abord être supprimée.

3. Mettez-vous à la place de votre client et suivez les étapes de l'interface utilisateur nécessaires pour terminer complètement un flux de fonctionnalités. Enregistrez les étapes d'un client pour effectuer un flux complet ou une action. Gardez une trace de ce que l'utilisateur devrait ou ne devrait pas voir ou interagir après chaque étape. Nous effectuerons des vérifications et des affirmations en cours de route pour nous assurer que les utilisateurs rencontrent les séquences d'événements appropriées pour leurs actions. Nous traduirons ensuite les vérifications d'intégrité en commandes et assertions automatisées.

4. Maintenez les modifications et automatisez les flux en ajoutant des sélecteurs spécifiques et en implémentant des objets de page (ou tout autre type de wrapper). Passez en revue les étapes que vous avez notées pour savoir comment manœuvrer et parcourir un flux de fonctionnalités. Ajoutez des sélecteurs plus spécifiques comme les attributs `data-hook` aux éléments avec lesquels l'utilisateur a interagi comme les boutons, les modaux, les entrées, les lignes de tableau, les alertes et les cartes. Si vous préférez, vous pouvez créer des objets de page, des wrappers ou des fichiers d'assistance avec des références à ces éléments via les sélecteurs que vous avez ajoutés. Vous pouvez ensuite implémenter des fonctions réutilisables pour interagir avec les éléments actionnables de la page.

5. Traduisez les étapes utilisateur que vous avez enregistrées en tests Cypress avec les assistants que vous avez créés. Dans les tests, nous nous connectons généralement à un utilisateur via l'API et préservons le cookie de session avant l'exécution de chaque cas de test pour rester connecté. Nous configurons ou supprimons ensuite les données de l'utilisateur via l'API pour avoir un point de départ cohérent. Avec tout en place, nous visitons la page que nous testerons directement pour notre fonctionnalité. Nous procédons à l'exécution d'étapes pour le flux, telles qu'un flux de création, de mise à jour ou de suppression, en affirmant ce qui doit se passer ou être visible sur la page en cours de route. Afin d'accélérer les tests et de réduire les irrégularités, évitez de réinitialiser ou de créer un état via l'interface utilisateur et contournez des éléments tels que la connexion via la page de connexion ou la suppression d'éléments via l'interface utilisateur pour vous concentrer sur les parties que vous souhaitez tester. Assurez-vous de toujours faire ces parties dans les crochets `before` ou `beforeEach`. Sinon, si vous avez utilisé les crochets `after` ou `afterEach`, les tests peuvent échouer entre les deux, ce qui empêche l'exécution de vos étapes de nettoyage et entraîne l'échec des exécutions de test suivantes.

6. Martelez et tamponnez la desquamation du test. Après avoir implémenté les tests et réussi plusieurs fois localement, il est tentant de configurer une demande d'extraction, de la fusionner immédiatement et d'exécuter les tests selon un calendrier avec le reste de votre suite de tests ou de les déclencher dans vos étapes de déploiement. Avant de faire ça :

    1. Tout d'abord, essayez de laisser l'utilisateur dans différents états et voyez si vos tests réussissent toujours pour vous assurer que vous avez les étapes de configuration appropriées.
    2. Ensuite, étudiez l'exécution de vos tests en parallèle lorsqu'ils sont déclenchés pendant l'un de vos flux de déploiement. Cela vous permet de voir si les ressources sont piétinées par les mêmes utilisateurs et s'il y a des conditions de concurrence.
    3. Ensuite, observez comment vos tests s'exécutent en mode sans tête dans un conteneur Docker pour voir si vous devrez peut-être augmenter les délais d'attente ou ajuster les sélecteurs.

L'objectif est de voir comment vos tests se comportent à travers des tests répétés dans différentes conditions et de les rendre aussi stables et cohérents que possible afin que nous passions moins de temps à corriger les tests et que nous nous concentrions davantage sur la détection de bogues réels dans nos environnements.

Voici un exemple de disposition standard de test Cypress dans laquelle nous avons créé une commande de support global de connexion appelée `cy.login (nom d'utilisateur, mot de passe).` Nous définissons le cookie explicitement et le préservons avant chaque cas de test afin que nous puissions rester connectés et aller directement aux pages que nous testons. Nous effectuons également une configuration ou démontons via l'API et contournons la page de connexion à chaque fois, comme indiqué ci-dessous.

Mettre fin aux pensées

En plus de comparer quelle solution E2E est la meilleure à utiliser, il est également important d'adopter l'état d'esprit approprié pour les tests E2E. Il est crucial de commencer par se poser des questions pour savoir si la fonctionnalité que vous souhaitez tester répond ou non aux exigences à automatiser. Il devrait y avoir des moyens pour vous de réinitialiser l'utilisateur ou les données à un certain état de manière fiable (comme via l'API) afin que vous puissiez vous concentrer sur ce que vous essayez de valider.

S'il n'existe aucun moyen fiable de réinitialiser votre utilisateur ou vos données au bon point de départ, vous devriez envisager de créer des outils et des API pour créer des utilisateurs avec certaines configurations. Vous pouvez également envisager de vous moquer des choses que vous pouvez contrôler pour rendre les tests aussi stables et cohérents que possible. Sinon, vous devriez considérer la valeur et les compromis avec votre équipe. Cette fonctionnalité est-elle celle que vous devriez laisser aux tests unitaires ou aux tests de régression manuels lorsque de nouveaux changements de code sont poussés ?

Pour les fonctionnalités que vous pouvez automatiser dans les tests E2E, il est souvent plus utile de couvrir les principaux flux de chemin heureux de vos utilisateurs. Encore une fois, rendez les choses reproductibles de manière opportune et cohérente et éliminez les irrégularités lorsque vous écrivez des tests E2E avec n'importe quel framework ou bibliothèque que vous désirez.

Pour plus d'informations sur les tests Cypress E2E spécifiques, consultez les ressources suivantes :

  • Vue d'ensemble de 1 000 pieds des tests d'écriture Cypress
  • TypeScript Toutes les choses dans vos tests Cypress
  • Gestion des flux de messagerie dans les tests Cypress
  • Idées pour configurer, organiser et consolider vos tests Cypress
  • Intégration des tests Cypress avec Docker, Buildkite et CICD