Mirage JS Deep Dive: Utilizarea Mirage JS și Cypress pentru testarea UI (Partea 4)
Publicat: 2022-03-10Unul dintre citatele mele preferate despre testarea software-ului este din documentația Flutter. Se spune:
„Cum vă puteți asigura că aplicația dvs. continuă să funcționeze pe măsură ce adăugați mai multe funcții sau modificați funcționalitatea existentă? Scriind teste.”
În această notă, această ultimă parte a seriei Mirage JS Deep Dive se va concentra pe utilizarea Mirage pentru a testa aplicația JavaScript front-end.
Notă : Acest articol presupune un mediu Cypress. Cypress este un cadru de testare pentru testarea UI. Cu toate acestea, puteți transfera cunoștințele aici în orice mediu sau cadru de testare a UI pe care îl utilizați.
Citiți părțile anterioare ale seriei:
- Partea 1: Înțelegerea modelelor și asocierilor Mirage JS
- Partea 2: Înțelegerea fabricilor, instalațiilor și serializatoarelor
- Partea 3: Înțelegerea timpului, a răspunsului și a transmiterii
UI Tests Primer
Testul UI sau al interfeței cu utilizatorul este o formă de testare de acceptare efectuată pentru a verifica fluxurile de utilizatori ale aplicației dvs. front-end. Accentul acestor tipuri de teste software se pune pe utilizatorul final, adică persoana reală care va interacționa cu aplicația dvs. web pe o varietate de dispozitive, de la desktop-uri, laptopuri la dispozitive mobile. Acești utilizatori vor interacționa cu aplicația dvs. folosind dispozitive de intrare, cum ar fi tastatura, mouse-ul sau ecranele tactile. Prin urmare, testele UI sunt scrise pentru a imita cât mai aproape posibil interacțiunea utilizatorului cu aplicația dvs.
Să luăm, de exemplu, un site de comerț electronic. Un scenariu tipic de testare a UI ar fi:
- Utilizatorul poate vizualiza lista de produse atunci când vizitează pagina de pornire.
Alte scenarii de testare a UI ar putea fi:
- Utilizatorul poate vedea numele unui produs pe pagina de detalii a produsului.
- Utilizatorul poate face clic pe butonul „Adaugă în coș”.
- Utilizatorul poate face checkout.
Ai înțeles ideea, nu?
Atunci când faceți teste UI, vă veți baza în mare parte pe stările dvs. de back-end, adică a returnat produsele sau a apărut o eroare? Rolul pe care Mirage îl joacă în acest sens este de a face acele stări ale serverului disponibile pentru a le modifica după cum aveți nevoie. Deci, în loc să faceți o solicitare reală către serverul dvs. de producție în testele dvs. de UI, faceți cererea către serverul simulat Mirage.
Pentru partea rămasă a acestui articol, vom efectua teste de interfață pe o interfață de utilizare a aplicației web de comerț electronic fictive. Asadar, haideti sa începem.
Primul nostru test UI
După cum sa spus mai devreme, acest articol presupune un mediu Cypress. Cypress face testarea UI pe web rapidă și ușoară. Puteți simula clicurile și navigarea și puteți vizita programatic rute în aplicația dvs. Consultați documentele pentru mai multe despre Cypress.
Deci, presupunând că Cypress și Mirage ne sunt disponibile, să începem prin a defini o funcție proxy pentru solicitarea dvs. API. Putem face acest lucru în fișierul support/index.js
al configurației noastre Cypress. Doar lipiți următorul cod în:
// 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])) }) }) } })
Apoi, în fișierul de bootstrapping al aplicației ( main.js
pentru Vue, index.js
pentru React), vom folosi Mirage pentru a trimite cererile API ale aplicației dvs. la funcția handleFromCypress
numai când Cypress rulează. Iată codul pentru asta:
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) }) }) }, }) }
Cu această configurare, oricând Cypress rulează, aplicația dvs. știe să folosească Mirage ca server simulat pentru toate solicitările API.
Să continuăm să scriem câteva teste UI. Vom începe prin a testa pagina noastră de pornire pentru a vedea dacă are 5 produse afișate. Pentru a face acest lucru în Cypress, trebuie să creăm un fișier homepage.test.js
în folderul tests
din rădăcina directorului proiectului. În continuare, îi vom spune lui Cypress să facă următoarele:
- Vizitați pagina principală, adică
/
traseu - Apoi afirmă dacă are li elemente cu clasa de
product
și verifică, de asemenea, dacă sunt 5 în numere.
Iată codul:
// homepage.test.js it('shows the products', () => { cy.visit('/'); cy.get('li.product').should('have.length', 5); });
S-ar putea să fi ghicit că acest test va eșua deoarece nu avem un server de producție care returnează 5 produse la aplicația noastră front-end. Deci ce facem? Ne batem joc de serverul din Mirage! Dacă aducem Mirage, acesta poate intercepta toate apelurile de rețea în testele noastre. Să facem asta mai jos și să pornim serverul Mirage înainte de fiecare test în funcția beforeEach
și, de asemenea, să îl închidem în funcția afterEach
. Funcțiile beforeEach
și afterEach
sunt ambele furnizate de Cypress și au fost puse la dispoziție astfel încât să puteți rula cod înainte și după fiecare rulare de testare în suita dvs. de teste - de unde și numele. Deci, să vedem codul pentru asta:
// 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) })
Bine, ajungem undeva; am importat serverul de la Mirage și îl pornim și îl închidem în funcțiile beforeEach
și, respectiv, afterEach
. Să ne batem joc de resursa noastră de produse.
// 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); });
Notă : puteți arunca oricând o privire la părțile anterioare ale acestei serii dacă nu înțelegeți fragmentele Mirage din fragmentul de cod de mai sus.
- Partea 1: Înțelegerea modelelor și asocierilor Mirage JS
- Partea 2: Înțelegerea fabricilor, instalațiilor și serializatoarelor
- Partea 3: Înțelegerea timpului, a răspunsului și a transmiterii
Bine, am început să dezvoltăm instanța Server prin crearea modelului de produs și, de asemenea, prin crearea unui handler de rută pentru ruta /api/products
. Cu toate acestea, dacă ne rulăm testele, va eșua deoarece nu avem încă niciun produs în baza de date Mirage.
Să populăm baza de date Mirage cu unele produse. Pentru a face acest lucru, am fi putut folosi metoda create()
pe instanța serverului nostru, dar crearea manuală a 5 produse pare destul de obositoare. Ar trebui să existe o cale mai bună.
Ah, da, există. Să folosim fabricile (după cum este explicat în a doua parte a acestei serii). Va trebui să ne creăm fabrica de produse astfel:
// 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); });
Apoi, în sfârșit, vom folosi createList()
pentru a crea rapid cele 5 produse pe care testul nostru trebuie să le treacă.
Să o facem:
// 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); });
Așa că atunci când rulăm testul, acesta trece!
Notă : După fiecare test, serverul Mirage este oprit și resetat, astfel încât niciuna dintre aceste stări nu se va scurge de-a lungul testelor.
Evitarea serverelor Mirage multiple
Dacă ați urmărit această serie, ați observa când am folosit Mirage în dezvoltare pentru a intercepta solicitările noastre de rețea; aveam un fișier server.js
în rădăcina aplicației noastre unde am configurat Mirage. În spiritul DRY (Don’t Repeat Yourself), cred că ar fi bine să utilizați acea instanță de server în loc să aveți două instanțe separate de Mirage atât pentru dezvoltare, cât și pentru testare. Pentru a face acest lucru (în cazul în care nu aveți deja un fișier server.js
), trebuie doar să creați unul în directorul src al proiectului.

Notă : Structura dvs. va diferi dacă utilizați un cadru JavaScript, dar ideea generală este să configurați fișierul server.js în rădăcina src a proiectului dumneavoastră.
Deci, cu această nouă structură, vom exporta o funcție în server.js
care este responsabilă pentru crearea instanței serverului nostru Mirage. Hai să facem asta:
// src/server.js export function makeServer() { /* Mirage code goes here */}
Să completăm implementarea funcției makeServer
eliminând serverul Mirage JS pe care l-am creat în homepage.test.js
și adăugându-l în corpul funcției 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; }
Acum tot ce trebuie să faceți este să importați makeServer
în testul dvs. Utilizarea unei singure instanțe Mirage Server este mai curată; în acest fel, nu trebuie să mențineți două instanțe de server atât pentru mediile de dezvoltare, cât și pentru mediile de testare.
După importarea funcției makeServer
, testul nostru ar trebui să arate acum astfel:
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); });
Deci avem acum un server central Mirage care ne servește atât în dezvoltare, cât și în testare. De asemenea, puteți utiliza funcția makeServer
pentru a porni Mirage în dezvoltare (vezi prima parte a acestei serii).
Codul dvs. Mirage nu ar trebui să-și găsească drumul în producție. Prin urmare, în funcție de configurația dvs. de construcție, ar trebui să porniți Mirage doar în timpul modului de dezvoltare.
Notă : Citiți articolul meu despre cum să configurați API Mocking cu Mirage și Vue.js pentru a vedea cum am făcut asta în Vue, astfel încât să puteți replica în orice cadru front-end pe care îl utilizați.
Mediul de testare
Mirage are două medii: dezvoltare (implicit) și testare . În modul de dezvoltare, serverul Mirage va avea un timp de răspuns implicit de 400 ms (pe care îl puteți personaliza. Vezi al treilea articol din această serie pentru asta), înregistrează toate răspunsurile serverului în consolă și încarcă semințele de dezvoltare.
Cu toate acestea, în mediul de testare, avem:
- 0 întârzieri pentru a ne menține testele rapide
- Mirage suprimă toate jurnalele pentru a nu polua jurnalele dvs. CI
- Mirage va ignora, de asemenea, funcția
seeds()
, astfel încât datele dvs. de semințe să poată fi utilizate numai pentru dezvoltare, dar să nu se scurgă în testele dvs. Acest lucru vă ajută să vă păstrați testele deterministe.
Să ne actualizăm makeServer
, astfel încât să putem beneficia de mediul de testare. Pentru a face asta, îl vom face să accepte un obiect cu opțiunea de mediu (îl vom dezvolta implicit și îl vom înlocui în testul nostru). server.js
-ul nostru ar trebui să arate acum astfel:
// 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; }
De asemenea, rețineți că transmitem opțiunea de mediu instanței de server Mirage folosind prescurtarea proprietății ES6. Acum, cu acest lucru în vigoare, să ne actualizăm testul pentru a înlocui valoarea mediului de testat. Testul nostru arată acum astfel:
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); });
Testarea AAA
Mirage încurajează un standard pentru testare numit abordarea de testare triplu-A sau AAA. Aceasta înseamnă Aranjare , Acționare și Afirmare . Puteți vedea deja această structură în testul nostru de mai sus:
it("shows all the products", function () { // ARRANGE server.createList("product", 5) // ACT cy.visit("/") // ASSERT cy.get("li.product").should("have.length", 5) })
S-ar putea să fie nevoie să spargeți acest model, dar de 9 ori din 10 ar trebui să funcționeze bine pentru testele dvs.
Să testăm erorile
Până acum, ne-am testat pagina de pornire pentru a vedea dacă are 5 produse, totuși, ce se întâmplă dacă serverul este oprit sau ceva nu a mers prost la preluarea produselor? Nu trebuie să așteptăm ca serverul să lucreze la modul în care ar arăta interfața noastră de utilizare într-un astfel de caz. Pur și simplu putem simula acel scenariu cu Mirage.
Să returnăm un 500 (eroare de server) când utilizatorul este pe pagina de pornire. După cum am văzut într-un articol anterior, pentru a personaliza răspunsurile Mirage, folosim clasa Response. Să-l importăm și să scriem testul nostru.
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"); });
Ce lume de flexibilitate! Pur și simplu anulăm răspunsul pe care Mirage îl va returna pentru a testa modul în care s-ar afișa interfața noastră de utilizare dacă nu reușește să preia produsele. Fișierul nostru general homepage.test.js
ar arăta acum astfel:
// 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"); });
Rețineți că modificarea pe care am făcut-o /api/products
se află doar în testul nostru. Aceasta înseamnă că funcționează așa cum am definit anterior când sunteți în modul de dezvoltare.
Așa că atunci când ne rulăm testele, ambele ar trebui să treacă.
Notă : cred că este demn de remarcat faptul că elementele pe care le solicităm în Cypress ar trebui să existe în interfața dvs. de utilizare front-end. Cypress nu creează elemente HTML pentru tine.
Testarea paginii cu detaliile produsului
În cele din urmă, să testăm interfața de utilizare a paginii cu detalii despre produs. Deci, pentru asta testăm:
- Utilizatorul poate vedea numele produsului pe pagina cu detaliile produsului
Să ajungem la asta. Mai întâi, creăm un nou test pentru a testa acest flux de utilizatori.
Iată testul:
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'); });
homepage.test.js
ar trebui, în sfârșit, să arate așa.
// 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'); });
Când rulați testele, toate trei ar trebui să treacă.
Încheierea
A fost distractiv să vă arăt interiorul Mirage JS în această serie. Sper că ați fost mai bine echipat pentru a începe să aveți o experiență de dezvoltare front-end mai bună, folosind Mirage pentru a vă bate joc de serverul back-end. De asemenea, sper că veți folosi cunoștințele din acest articol pentru a scrie mai multe teste de acceptare/UI/end-to-end pentru aplicațiile dumneavoastră front-end.
- Partea 1: Înțelegerea modelelor și asocierilor Mirage JS
- Partea 2: Înțelegerea fabricilor, instalațiilor și serializatoarelor
- Partea 3: Înțelegerea timpului, a răspunsului și a transmiterii
- Partea 4: Utilizarea Mirage JS și Cypress pentru testarea UI