Mirage JS Deep Dive : Utilisation de Mirage JS et Cypress pour les tests d'interface utilisateur (Partie 4)

Publié: 2022-03-10
Résumé rapide ↬ Dans cette dernière partie de la série Mirage JS Deep Dive, nous mettrons tout ce que nous avons appris dans la série précédente pour apprendre à effectuer des tests d'interface utilisateur avec Mirage JS.

L'une de mes citations préférées sur les tests de logiciels provient de la documentation de Flutter. Ça dit:

"Comment pouvez-vous vous assurer que votre application continue de fonctionner lorsque vous ajoutez des fonctionnalités ou modifiez des fonctionnalités existantes ? En écrivant des tests.

Sur cette note, cette dernière partie de la série Mirage JS Deep Dive se concentrera sur l'utilisation de Mirage pour tester votre application frontale JavaScript.

Remarque : cet article suppose un environnement Cypress. Cypress est un cadre de test pour les tests d'interface utilisateur. Vous pouvez cependant transférer les connaissances ici vers n'importe quel environnement ou framework de test d'interface utilisateur que vous utilisez.

Lisez les parties précédentes de la série :

  • Partie 1 : Comprendre les modèles et les associations de Mirage JS
  • Partie 2 : Comprendre les usines, les montages et les sérialiseurs
  • Partie 3 : Comprendre le timing, la réponse et le passthrough

Introduction aux tests d'interface utilisateur

Le test d'interface utilisateur ou d'interface utilisateur est une forme de test d'acceptation effectué pour vérifier les flux d' utilisateurs de votre application frontale. L'accent de ces types de tests logiciels est mis sur l'utilisateur final, c'est-à-dire la personne qui interagira avec votre application Web sur une variété d'appareils allant des ordinateurs de bureau, des ordinateurs portables aux appareils mobiles. Ces utilisateurs s'interfaceraient ou interagiraient avec votre application à l'aide de périphériques d'entrée tels qu'un clavier, une souris ou des écrans tactiles. Les tests d'interface utilisateur sont donc écrits pour imiter le plus possible l'interaction de l' utilisateur avec votre application.

Prenons l'exemple d'un site e-commerce. Un scénario de test d'interface utilisateur typique serait :

  • L'utilisateur peut consulter la liste des produits lorsqu'il visite la page d'accueil.

D'autres scénarios de test d'interface utilisateur peuvent être :

  • L'utilisateur peut voir le nom d'un produit sur la page de détail du produit.
  • L'utilisateur peut cliquer sur le bouton "ajouter au panier".
  • L'utilisateur peut payer.

Vous avez l'idée, non?

En faisant des tests d'interface utilisateur, vous vous fierez principalement à vos états de back-end, c'est-à-dire qu'il a renvoyé les produits ou une erreur ? Le rôle que joue Mirage dans ce domaine est de rendre ces états de serveur disponibles pour que vous puissiez les modifier selon vos besoins. Ainsi, au lieu de faire une demande réelle à votre serveur de production dans vos tests d'interface utilisateur, vous faites la demande au serveur fictif Mirage.

Pour la suite de cet article, nous effectuerons des tests d'interface utilisateur sur une interface utilisateur d'application Web de commerce électronique fictive. Alors, commençons.

Plus après saut! Continuez à lire ci-dessous ↓

Notre premier test d'interface utilisateur

Comme indiqué précédemment, cet article suppose un environnement Cypress. Cypress permet de tester rapidement et facilement l'interface utilisateur sur le Web. Vous pouvez simuler les clics et la navigation et vous pouvez visiter par programmation des itinéraires dans votre application. Voir les docs pour en savoir plus sur Cypress.

Donc, en supposant que Cypress et Mirage soient disponibles pour nous, commençons par définir une fonction proxy pour votre requête API. Nous pouvons le faire dans le fichier support/index.js de notre configuration Cypress. Collez simplement le code suivant dans :

 // cypress/support/index.js Cypress.on("window:before:load", (win) => { win.handleFromCypress = function (request) { return fetch(request.url, { method: request.method, headers: request.requestHeaders, body: request.requestBody, }).then((res) => { let content = res.headers.map["content-type"] === "application/json" ? res.json() : res.text() return new Promise((resolve) => { content.then((body) => resolve([res.status, res.headers, body])) }) }) } })

Ensuite, dans le fichier d'amorçage de votre application ( main.js pour Vue, index.js pour React), nous utiliserons Mirage pour envoyer par proxy les requêtes API de votre application à la fonction handleFromCypress uniquement lorsque Cypress est en cours d'exécution. Voici le code pour cela :

 import { Server, Response } from "miragejs" if (window.Cypress) { new Server({ environment: "test", routes() { let methods = ["get", "put", "patch", "post", "delete"] methods.forEach((method) => { this[method]("/*", async (schema, request) => { let [status, headers, body] = await window.handleFromCypress(request) return new Response(status, headers, body) }) }) }, }) }

Avec cette configuration, chaque fois que Cypress est en cours d'exécution, votre application sait utiliser Mirage comme serveur fictif pour toutes les demandes d'API.

Continuons à écrire quelques tests d'interface utilisateur. Nous allons commencer par tester notre page d'accueil pour voir si elle affiche 5 produits . Pour ce faire dans Cypress, nous devons créer un fichier homepage.test.js dans le dossier tests à la racine de votre répertoire de projet. Ensuite, nous dirons à Cypress de faire ce qui suit :

  • Visitez la page d'accueil, c'est-à-dire / itinéraire
  • Ensuite, affirmez s'il a li éléments avec la classe de product et vérifiez également s'ils sont au nombre de 5.

Voici le code :

 // homepage.test.js it('shows the products', () => { cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Vous avez peut-être deviné que ce test échouerait car nous n'avons pas de serveur de production renvoyant 5 produits à notre application frontale. Alors que faisons-nous? Nous nous moquons du serveur dans Mirage ! Si nous apportons Mirage, il peut intercepter tous les appels réseau lors de nos tests. Faisons cela ci-dessous et démarrons le serveur Mirage avant chaque test dans la fonction beforeEach et arrêtons-le également dans la fonction afterEach . Les fonctions beforeEach et afterEach sont toutes deux fournies par Cypress et elles ont été mises à disposition afin que vous puissiez exécuter du code avant et après chaque exécution de test dans votre suite de tests - d'où le nom. Voyons donc le code pour ceci:

 // homepage.test.js import { Server } from "miragejs" let server beforeEach(() => { server = new Server() }) afterEach(() => { server.shutdown() }) it("shows the products", function () { cy.visit("/") cy.get("li.product").should("have.length", 5) })

D'accord, nous arrivons quelque part; nous avons importé le serveur de Mirage et nous le démarrons et l'arrêtons respectivement dans les fonctions beforeEach et afterEach . Allons nous moquer de notre ressource produit.

 // homepage.test.js import { Server, Model } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Remarque : Vous pouvez toujours jeter un coup d'œil aux parties précédentes de cette série si vous ne comprenez pas les bits Mirage de l'extrait de code ci-dessus.

  • Partie 1 : Comprendre les modèles et les associations de Mirage JS
  • Partie 2 : Comprendre les usines, les montages et les sérialiseurs
  • Partie 3 : Comprendre le timing, la réponse et le passthrough

D'accord, nous avons commencé à étoffer notre instance de serveur en créant le modèle de produit et également en créant le gestionnaire de route pour la route /api/products . Cependant, si nous exécutons nos tests, cela échouera car nous n'avons pas encore de produits dans la base de données Mirage.

Remplissons la base de données Mirage avec quelques produits. Pour ce faire, nous aurions pu utiliser la méthode create() sur notre instance de serveur, mais créer 5 produits à la main semble assez fastidieux. Il devrait y avoir un meilleur moyen.

Ah oui, il y en a. Utilisons les usines (comme expliqué dans la deuxième partie de cette série). Nous devrons créer notre usine de produits comme suit :

 // homepage.test.js import { Server, Model, Factory } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}` } }) }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Puis, enfin, nous utiliserons createList() pour créer rapidement les 5 produits que notre test doit réussir.

Faisons cela:

 // homepage.test.js import { Server, Model, Factory } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}` } }) }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList("product", 5) cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Alors quand on lance notre test, ça passe !

Remarque : après chaque test, le serveur de Mirage est arrêté et réinitialisé, donc aucun de ces états ne fuira d'un test à l'autre.

Éviter plusieurs serveurs Mirage

Si vous avez suivi cette série, vous avez remarqué que nous utilisions Mirage en développement pour intercepter nos requêtes réseau ; nous avions un fichier server.js à la racine de notre application où nous avons configuré Mirage. Dans l'esprit de DRY (Don't Repeat Yourself), je pense qu'il serait bon d'utiliser cette instance de serveur au lieu d'avoir deux instances distinctes de Mirage pour le développement et les tests. Pour ce faire (au cas où vous n'auriez pas encore de fichier server.js ), créez-en simplement un dans le répertoire src de votre projet.

Remarque : Votre structure sera différente si vous utilisez un framework JavaScript, mais l'idée générale est de configurer le fichier server.js dans la racine src de votre projet.

Donc, avec cette nouvelle structure, nous allons exporter une fonction dans server.js qui est responsable de la création de notre instance de serveur Mirage. Faisons cela:

 // src/server.js export function makeServer() { /* Mirage code goes here */}

Terminons l'implémentation de la fonction makeServer en supprimant le serveur Mirage JS que nous avons créé dans homepage.test.js et en l'ajoutant au corps de la fonction makeServer :

 import { Server, Model, Factory } from 'miragejs'; export function makeServer() { let server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}`; }, }), }, routes() { this.namespace = 'api'; this.get('/products', ({ products }) => { return products.all(); }); }, seeds(server) { server.createList('product', 5); }, }); return server; }

Il ne vous reste plus qu'à importer makeServer dans votre test. L'utilisation d'une seule instance de Mirage Server est plus propre ; de cette façon, vous n'avez pas à maintenir deux instances de serveur pour les environnements de développement et de test.

Après avoir importé la fonction makeServer , notre test devrait maintenant ressembler à ceci :

 import { makeServer } from '/path/to/server'; let server; beforeEach(() => { server = makeServer(); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Nous avons donc maintenant un serveur Mirage central qui nous sert à la fois pour le développement et les tests. Vous pouvez également utiliser la fonction makeServer pour démarrer Mirage en développement (voir la première partie de cette série).

Votre code Mirage ne devrait pas être mis en production. Par conséquent, selon la configuration de votre build, vous n'aurez besoin de démarrer Mirage qu'en mode développement.

Remarque : Lisez mon article sur la configuration de l'API Mocking avec Mirage et Vue.js pour voir comment j'ai fait cela dans Vue afin que vous puissiez répliquer dans n'importe quel framework frontal que vous utilisez.

Environnement de test

Mirage a deux environnements : development (par défaut) et test . En mode développement, le serveur Mirage aura un temps de réponse par défaut de 400 ms (que vous pouvez personnaliser. Voir le troisième article de cette série pour cela), enregistre toutes les réponses du serveur sur la console et charge les graines de développement.

Cependant, dans l'environnement de test, nous avons :

  • 0 retard pour garder nos tests rapides
  • Mirage supprime tous les logs pour ne pas polluer vos logs CI
  • Mirage ignorera également la fonction seeds() afin que vos données de départ puissent être utilisées uniquement pour le développement mais ne fuient pas dans vos tests. Cela permet de garder vos tests déterministes.

Mettons à jour notre makeServer afin de pouvoir bénéficier de l'environnement de test. Pour ce faire, nous lui ferons accepter un objet avec l'option d'environnement (nous le définirons par défaut sur développement et le remplacerons dans notre test). Notre server.js devrait maintenant ressembler à ceci :

 // src/server.js import { Server, Model, Factory } from 'miragejs'; export function makeServer({ environment = 'development' } = {}) { let server = new Server({ environment, models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}`; }, }), }, routes() { this.namespace = 'api'; this.get('/products', ({ products }) => { return products.all(); }); }, seeds(server) { server.createList('product', 5); }, }); return server; }

Notez également que nous transmettons l'option d'environnement à l'instance de serveur Mirage en utilisant le raccourci de propriété ES6. Maintenant que cela est en place, mettons à jour notre test pour remplacer la valeur d'environnement à tester. Notre test ressemble maintenant à ceci :

 import { makeServer } from '/path/to/server'; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Test AAA

Mirage encourage une norme de test appelée approche de test triple-A ou AAA. Cela signifie organiser , agir et affirmer . Vous pouvez déjà voir cette structure dans notre test ci-dessus :

 it("shows all the products", function () { // ARRANGE server.createList("product", 5) // ACT cy.visit("/") // ASSERT cy.get("li.product").should("have.length", 5) })

Vous devrez peut-être casser ce schéma, mais 9 fois sur 10, cela devrait fonctionner correctement pour vos tests.

Testons les erreurs

Jusqu'à présent, nous avons testé notre page d'accueil pour voir si elle contient 5 produits, cependant, que se passe-t-il si le serveur est en panne ou si quelque chose ne va pas avec la récupération des produits ? Nous n'avons pas besoin d'attendre que le serveur soit en panne pour travailler sur l'apparence de notre interface utilisateur dans un tel cas. Nous pouvons simplement simuler ce scénario avec Mirage.

Renvoyons un 500 (erreur de serveur) lorsque l'utilisateur est sur la page d'accueil. Comme nous l'avons vu dans un article précédent, pour personnaliser les réponses de Mirage, nous utilisons la classe Response. Importons-le et écrivons notre test.

 homepage.test.js import { Response } from "miragejs" it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); });

Quel monde de flexibilité ! Nous remplaçons simplement la réponse que Mirage renverrait afin de tester l'affichage de notre interface utilisateur en cas d'échec de la récupération des produits. Notre fichier global homepage.test.js ressemblerait maintenant à ceci :

 // homepage.test.js import { Response } from 'miragejs'; import { makeServer } from 'path/to/server'; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); }); it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); });

Notez que la modification que nous avons apportée au gestionnaire /api/products n'existe que dans notre test. Cela signifie que cela fonctionne comme nous l'avons défini précédemment lorsque vous êtes en mode développement.

Ainsi, lorsque nous exécutons nos tests, les deux devraient réussir.

Remarque : Je pense qu'il est utile de noter que les éléments que nous recherchons dans Cypress doivent exister dans votre interface utilisateur frontale. Cypress ne crée pas d'éléments HTML pour vous.

Test de la page de détail du produit

Enfin, testons l'interface utilisateur de la page de détail du produit. Voici donc ce que nous testons :

  • L'utilisateur peut voir le nom du produit sur la page de détail du produit

Allons-y. Tout d'abord, nous créons un nouveau test pour tester ce flux d'utilisateurs.

Voici l'essai :

 it("shows the product's name on the detail route", function() { let product = this.server.create('product', { name: 'Korg Piano', }); cy.visit(`/${product.id}`); cy.get('h1').should('contain', 'Korg Piano'); });

Votre homepage.test.js devrait enfin ressembler à ceci.

 // homepage.test.js import { Response } from 'miragejs'; import { makeServer } from 'path/to/server; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { console.log(server); server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); }); it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); }); it("shows the product's name on the detail route", function() { let product = server.create('product', { name: 'Korg Piano', }); cy.visit(`/${product.id}`); cy.get('h1').should('contain', 'Korg Piano'); });

Lorsque vous exécutez vos tests, les trois doivent réussir.

Emballer

C'était amusant de vous montrer les entrailles de Mirage JS dans cette série. J'espère que vous avez été mieux équipé pour commencer à avoir une meilleure expérience de développement frontal en utilisant Mirage pour simuler votre serveur principal. J'espère également que vous utiliserez les connaissances de cet article pour écrire davantage de tests d'acceptation/d'interface utilisateur/de bout en bout pour vos applications frontales.

  • Partie 1 : Comprendre les modèles et les associations de Mirage JS
  • Partie 2 : Comprendre les usines, les montages et les sérialiseurs
  • Partie 3 : Comprendre le timing, la réponse et le passthrough
  • Partie 4 : Utilisation de Mirage JS et Cypress pour les tests d'interface utilisateur