Mirage JS Deep Dive: utilizzo di Mirage JS e Cypress per i test dell'interfaccia utente (parte 4)
Pubblicato: 2022-03-10Una delle mie citazioni preferite sui test del software proviene dalla documentazione di Flutter. Dice:
"Come puoi assicurarti che la tua app continui a funzionare quando aggiungi più funzionalità o modifichi le funzionalità esistenti? Scrivendo dei test”.
In questa nota, quest'ultima parte della serie Mirage JS Deep Dive si concentrerà sull'utilizzo di Mirage per testare la tua applicazione front-end JavaScript.
Nota : questo articolo presuppone un ambiente Cypress. Cypress è un framework di test per i test dell'interfaccia utente. Tuttavia, puoi trasferire le conoscenze qui a qualsiasi ambiente di test dell'interfaccia utente o framework che utilizzi.
Leggi le parti precedenti della serie:
- Parte 1: Comprensione dei modelli e delle associazioni Mirage JS
- Parte 2: Comprensione di fabbriche, dispositivi e serializzatori
- Parte 3: Capire i tempi, la risposta e il passthrough
Primer per i test dell'interfaccia utente
Il test dell'interfaccia utente o dell'interfaccia utente è una forma di test di accettazione eseguita per verificare i flussi utente dell'applicazione front-end. L'enfasi di questo tipo di test del software è sull'utente finale che è la persona reale che interagirà con l'applicazione Web su una varietà di dispositivi che vanno da desktop, laptop a dispositivi mobili. Questi utenti si interfacciano o interagiscono con l'applicazione utilizzando dispositivi di input come tastiera, mouse o touch screen. I test dell'interfaccia utente, pertanto, vengono scritti per simulare l'interazione dell'utente con l'applicazione il più vicino possibile.
Prendiamo ad esempio un sito di e-commerce. Un tipico scenario di test dell'interfaccia utente sarebbe:
- L'utente può visualizzare l'elenco dei prodotti visitando la home page.
Altri scenari di test dell'interfaccia utente potrebbero essere:
- L'utente può vedere il nome di un prodotto nella pagina dei dettagli del prodotto.
- L'utente può fare clic sul pulsante "aggiungi al carrello".
- L'utente può effettuare il check-out.
Hai avuto l'idea, vero?
Nell'effettuare i test dell'interfaccia utente, farai principalmente affidamento sui tuoi stati di back-end, ad esempio ha restituito i prodotti o un errore? Il ruolo svolto da Mirage in questo è quello di rendere disponibili quegli stati del server che puoi modificare di cui hai bisogno. Quindi, invece di fare una richiesta effettiva al tuo server di produzione nei test dell'interfaccia utente, fai la richiesta al server fittizio Mirage.
Per la parte restante di questo articolo, eseguiremo test dell'interfaccia utente su un'interfaccia utente fittizia di un'applicazione Web di e-commerce. Quindi iniziamo.
Il nostro primo test dell'interfaccia utente
Come affermato in precedenza, questo articolo presuppone un ambiente Cypress. Cypress rende il test dell'interfaccia utente sul Web facile e veloce. Puoi simulare clic e navigazione e puoi visitare i percorsi in modo programmatico nella tua applicazione. Vedere i documenti per ulteriori informazioni su Cypress.
Quindi, supponendo che Cypress e Mirage siano disponibili per noi, iniziamo definendo una funzione proxy per la tua richiesta API. Possiamo farlo nel file support/index.js
della nostra configurazione di Cypress. Basta incollare il seguente codice in:
// 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])) }) }) } })
Quindi, nel file di bootstrap dell'app ( main.js
per Vue, index.js
per React), utilizzeremo Mirage per inoltrare le richieste API dell'app alla funzione handleFromCypress
solo quando Cypress è in esecuzione. Ecco il codice per quello:
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) }) }) }, }) }
Con questa configurazione, ogni volta che Cypress è in esecuzione, la tua app sa di utilizzare Mirage come server fittizio per tutte le richieste API.
Continuiamo a scrivere alcuni test dell'interfaccia utente. Inizieremo testando la nostra home page per vedere se ha 5 prodotti visualizzati. Per fare ciò in Cypress, dobbiamo creare un file homepage.test.js
nella cartella tests
nella radice della directory del tuo progetto. Successivamente, diremo a Cypress di fare quanto segue:
- Visita la home page cioè
/
percorso - Quindi asserisci se ha elementi li con la classe di
product
e controlla anche se sono 5 in numeri.
Ecco il codice:
// homepage.test.js it('shows the products', () => { cy.visit('/'); cy.get('li.product').should('have.length', 5); });
Potresti aver intuito che questo test avrebbe fallito perché non abbiamo un server di produzione che restituisce 5 prodotti alla nostra applicazione front-end. Quindi cosa facciamo? Prendiamo in giro il server in Mirage! Se portiamo Mirage, può intercettare tutte le chiamate di rete nei nostri test. Facciamolo di seguito e avviamo il server Mirage prima di ogni test nella funzione beforeEach
e spegnilo anche nella funzione afterEach
. Le funzioni beforeEach
e afterEach
sono entrambe fornite da Cypress e sono state rese disponibili in modo da poter eseguire il codice prima e dopo ogni test eseguito nella tua suite di test, da cui il nome. Quindi vediamo il codice per questo:
// 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) })
Ok, stiamo arrivando da qualche parte; abbiamo importato il server da Mirage e lo stiamo avviando e spegnendolo rispettivamente nelle funzioni beforeEach
e afterEach
. Andiamo a prendere in giro la nostra risorsa di prodotto.
// 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); });
Nota : puoi sempre dare un'occhiata alle parti precedenti di questa serie se non capisci i bit Mirage del frammento di codice sopra.
- Parte 1: Comprensione dei modelli e delle associazioni Mirage JS
- Parte 2: Comprensione di fabbriche, dispositivi e serializzatori
- Parte 3: Capire i tempi, la risposta e il passthrough
Ok, abbiamo iniziato a potenziare la nostra istanza Server creando il modello di prodotto e anche creando il gestore di route per la route /api/products
. Tuttavia, se eseguiamo i nostri test, fallirà perché non abbiamo ancora alcun prodotto nel database Mirage.
Popoliamo il database Mirage con alcuni prodotti. Per fare ciò, avremmo potuto utilizzare il metodo create()
sulla nostra istanza del server, ma creare 5 prodotti a mano sembra piuttosto noioso. Dovrebbe esserci un modo migliore.
Eh si, c'è. Utilizziamo le fabbriche (come spiegato nella seconda parte di questa serie). Avremo bisogno di creare la nostra fabbrica di prodotti in questo modo:
// 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); });
Quindi, infine, utilizzeremo createList()
per creare rapidamente i 5 prodotti che il nostro test deve superare.
Facciamolo:
// 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); });
Quindi, quando eseguiamo il nostro test, passa!
Nota : dopo ogni test, il server di Mirage viene arrestato e ripristinato, quindi nessuno di questi stati perderà tra i test.

Evitare più server Mirage
Se hai seguito questa serie, noterai quando stavamo utilizzando Mirage in fase di sviluppo per intercettare le nostre richieste di rete; avevamo un file server.js
nella radice della nostra app in cui abbiamo configurato Mirage. Nello spirito di DRY (Don't Repeat Yourself), penso che sarebbe utile utilizzare quell'istanza del server invece di avere due istanze separate di Mirage sia per lo sviluppo che per i test. Per fare ciò (nel caso in cui non si disponga già di un file server.js
), basta crearne uno nella directory src del progetto.
Nota : la tua struttura sarà diversa se stai utilizzando un framework JavaScript, ma l'idea generale è quella di configurare il file server.js nella radice src del tuo progetto.
Quindi, con questa nuova struttura, esporteremo una funzione in server.js
che è responsabile della creazione della nostra istanza del server Mirage. Facciamolo:
// src/server.js export function makeServer() { /* Mirage code goes here */}
Completiamo l'implementazione della funzione makeServer
rimuovendo il server Mirage JS che abbiamo creato in homepage.test.js
e aggiungendolo al corpo della funzione 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; }
Ora tutto ciò che devi fare è importare makeServer
nel tuo test. L'utilizzo di una singola istanza di Mirage Server è più pulito; in questo modo non è necessario mantenere due istanze del server sia per gli ambienti di sviluppo che per quelli di test.
Dopo aver importato la funzione makeServer
, il nostro test dovrebbe ora assomigliare a questo:
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); });
Quindi ora abbiamo un server Mirage centrale che ci serve sia per lo sviluppo che per i test. Puoi anche usare la funzione makeServer
per avviare Mirage in fase di sviluppo (vedi prima parte di questa serie).
Il tuo codice Mirage non dovrebbe trovare la sua strada nella produzione. Pertanto, a seconda della configurazione della build, è necessario avviare Mirage solo durante la modalità di sviluppo.
Nota : leggi il mio articolo su come configurare API Mocking con Mirage e Vue.js per vedere come l'ho fatto in Vue in modo da poterlo replicare in qualsiasi framework front-end in uso.
Ambiente di prova
Mirage ha due ambienti: sviluppo (predefinito) e test . In modalità di sviluppo, il server Mirage avrà un tempo di risposta predefinito di 400 ms (che puoi personalizzare. Vedi il terzo articolo di questa serie per questo), registra tutte le risposte del server sulla console e carica i semi di sviluppo.
Tuttavia, nell'ambiente di test, abbiamo:
- 0 ritardi per mantenere i nostri test veloci
- Mirage sopprime tutti i registri in modo da non inquinare i registri CI
- Mirage ignorerà anche la funzione
seeds()
in modo che i tuoi dati seed possano essere utilizzati esclusivamente per lo sviluppo ma non perdano nei test. Questo aiuta a mantenere i tuoi test deterministici.
Aggiorniamo il nostro makeServer
in modo da poter beneficiare dell'ambiente di test. Per fare ciò, faremo accettare un oggetto con l'opzione dell'ambiente (lo faremo per impostazione predefinita su sviluppo e lo sovrascriveremo nel nostro test). Il nostro server.js
ora dovrebbe assomigliare a questo:
// 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; }
Si noti inoltre che stiamo passando l'opzione dell'ambiente all'istanza del server Mirage utilizzando la scorciatoia della proprietà ES6. Ora con questo in atto, aggiorniamo il nostro test per sovrascrivere il valore dell'ambiente da testare. Il nostro test ora si presenta così:
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 incoraggia uno standard per i test chiamato approccio di test tripla A o AAA. Questo sta per Arrange , Act e Assert . Potresti già vedere questa struttura nel nostro test sopra:
it("shows all the products", function () { // ARRANGE server.createList("product", 5) // ACT cy.visit("/") // ASSERT cy.get("li.product").should("have.length", 5) })
Potrebbe essere necessario interrompere questo schema, ma 9 volte su 10 dovrebbe funzionare perfettamente per i tuoi test.
Testiamo gli errori
Finora, abbiamo testato la nostra home page per vedere se ha 5 prodotti, tuttavia, cosa succede se il server è inattivo o qualcosa è andato storto durante il recupero dei prodotti? Non è necessario attendere che il server sia inattivo per funzionare su come sarebbe la nostra interfaccia utente in questo caso. Possiamo semplicemente simulare quello scenario con Mirage.
Restituiamo un 500 (errore del server) quando l'utente è sulla home page. Come abbiamo visto in un precedente articolo, per personalizzare le risposte Mirage utilizziamo la classe Response. Importiamolo e scriviamo il nostro 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"); });
Che mondo di flessibilità! Abbiamo semplicemente ignorato la risposta che Mirage avrebbe restituito per testare come verrebbe visualizzata la nostra interfaccia utente se non riuscisse a recuperare i prodotti. Il nostro file homepage.test.js
in generale sarebbe ora simile a questo:
// 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"); });
Nota che la modifica che abbiamo apportato al gestore /api/products
vive solo nel nostro test. Ciò significa che funziona come abbiamo definito in precedenza quando sei in modalità di sviluppo.
Quindi, quando eseguiamo i nostri test, entrambi dovrebbero passare.
Nota : credo che sia degno di nota che gli elementi che stiamo interrogando in Cypress dovrebbero esistere nella tua interfaccia utente front-end. Cypress non crea elementi HTML per te.
Testare la pagina dei dettagli del prodotto
Infine, testiamo l'interfaccia utente della pagina dei dettagli del prodotto. Quindi questo è ciò per cui stiamo testando:
- L'utente può vedere il nome del prodotto nella pagina dei dettagli del prodotto
Andiamo a questo. Innanzitutto, creiamo un nuovo test per testare questo flusso utente.
Ecco la prova:
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'); });
La tua homepage.test.js
dovrebbe finalmente assomigliare a questa.
// 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'); });
Quando esegui i test, tutti e tre dovrebbero passare.
Avvolgendo
È stato divertente mostrarti i segreti di Mirage JS in questa serie. Spero che tu sia stato meglio attrezzato per iniziare ad avere una migliore esperienza di sviluppo front-end utilizzando Mirage per deridere il tuo server back-end. Spero inoltre che utilizzerai le conoscenze di questo articolo per scrivere più test di accettazione/interfaccia utente/end-to-end per le tue applicazioni front-end.
- Parte 1: Comprensione dei modelli e delle associazioni Mirage JS
- Parte 2: Comprensione di fabbriche, dispositivi e serializzatori
- Parte 3: Capire i tempi, la risposta e il passthrough
- Parte 4: utilizzo di Mirage JS e Cypress per i test dell'interfaccia utente