Mirage JS Deep Dive: Verwendung von Mirage JS und Cypress für UI-Tests (Teil 4)

Veröffentlicht: 2022-03-10
Kurze Zusammenfassung ↬ In diesem letzten Teil der Mirage JS Deep Dive-Serie werden wir alles, was wir in der vorherigen Serie gelernt haben, in das Erlernen der Durchführung von UI-Tests mit Mirage JS einfließen lassen.

Eines meiner Lieblingszitate über Softwaretests stammt aus der Flutter-Dokumentation. Es sagt:

„Wie können Sie sicherstellen, dass Ihre App weiterhin funktioniert, wenn Sie weitere Funktionen hinzufügen oder bestehende Funktionen ändern? Durch das Schreiben von Tests.“

In diesem Sinne konzentriert sich dieser letzte Teil der Mirage JS Deep Dive-Serie auf die Verwendung von Mirage zum Testen Ihrer JavaScript-Front-End-Anwendung.

Hinweis : Dieser Artikel geht von einer Cypress-Umgebung aus. Cypress ist ein Testframework für UI-Tests. Sie können das Wissen hier jedoch auf eine beliebige UI-Testumgebung oder ein beliebiges Framework übertragen, das Sie verwenden.

Lesen Sie frühere Teile der Serie:

  • Teil 1: Verständnis von Mirage JS-Modellen und -Verknüpfungen
  • Teil 2: Factories, Fixtures und Serializer verstehen
  • Teil 3: Timing, Reaktion und Passthrough verstehen

UI-Tests-Primer

Der UI- oder User Interface-Test ist eine Form des Akzeptanztests, mit dem die Benutzerabläufe Ihrer Front-End-Anwendung überprüft werden. Der Schwerpunkt dieser Art von Softwaretests liegt auf dem Endbenutzer, dh der tatsächlichen Person, die mit Ihrer Webanwendung auf einer Vielzahl von Geräten interagiert, von Desktops über Laptops bis hin zu mobilen Geräten. Diese Benutzer würden über Eingabegeräte wie Tastatur, Maus oder Touchscreens mit Ihrer Anwendung interagieren oder mit ihr interagieren. UI-Tests werden daher so geschrieben, dass sie die Benutzerinteraktion mit Ihrer Anwendung so genau wie möglich nachahmen.

Nehmen wir zum Beispiel eine E-Commerce-Website. Ein typisches UI-Testszenario wäre:

  • Die Produktliste kann der Nutzer beim Besuch der Homepage einsehen.

Andere UI-Testszenarien könnten sein:

  • Der Benutzer kann den Namen eines Produkts auf der Detailseite des Produkts sehen.
  • Der Benutzer kann auf die Schaltfläche „In den Warenkorb“ klicken.
  • Der Benutzer kann zur Kasse gehen.

Sie haben die Idee, richtig?

Beim Durchführen von UI-Tests verlassen Sie sich hauptsächlich auf Ihre Backend-Zustände, dh hat es die Produkte oder einen Fehler zurückgegeben? Die Rolle, die Mirage dabei spielt, besteht darin, diese Serverzustände für Sie zur Verfügung zu stellen, damit Sie sie nach Bedarf anpassen können. Anstatt also in Ihren UI-Tests eine tatsächliche Anfrage an Ihren Produktionsserver zu stellen, stellen Sie die Anfrage an den Mirage-Mock-Server.

Für den verbleibenden Teil dieses Artikels führen wir UI-Tests auf einer fiktiven UI einer E-Commerce-Webanwendung durch. Also lasst uns anfangen.

Mehr nach dem Sprung! Lesen Sie unten weiter ↓

Unser erster UI-Test

Wie bereits erwähnt, geht dieser Artikel von einer Cypress-Umgebung aus. Cypress macht das Testen der Benutzeroberfläche im Web schnell und einfach. Sie können Klicks und Navigation simulieren und Routen in Ihrer Anwendung programmgesteuert besuchen. Weitere Informationen zu Cypress finden Sie in der Dokumentation.

Angenommen, Cypress und Mirage stehen uns zur Verfügung, beginnen wir damit, eine Proxy-Funktion für Ihre API-Anfrage zu definieren. Wir können dies in der Datei support/index.js unseres Cypress-Setups tun. Fügen Sie einfach den folgenden Code ein:

 // 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])) }) }) } })

Dann verwenden wir in Ihrer App-Bootstrapping-Datei ( main.js für Vue, index.js für React) Mirage, um die API-Anforderungen Ihrer App nur dann an die handleFromCypress Funktion weiterzuleiten, wenn Cypress ausgeführt wird. Hier ist der Code dafür:

 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) }) }) }, }) }

Mit dieser Einrichtung weiß Ihre App, wann immer Cypress ausgeführt wird, Mirage als Mock-Server für alle API-Anforderungen zu verwenden.

Lassen Sie uns mit dem Schreiben einiger UI-Tests fortfahren. Wir beginnen damit, unsere Homepage zu testen, um zu sehen, ob 5 Produkte angezeigt werden. Dazu müssen wir in Cypress eine Datei „ homepage.test.js “ im Ordner „ tests “ im Stammverzeichnis Ihres Projektverzeichnisses erstellen. Als Nächstes weisen wir Cypress an, Folgendes zu tun:

  • Besuchen Sie die Homepage dh / route
  • Bestätigen Sie dann, ob es li Elemente mit der product hat, und prüfen Sie auch, ob es sich um 5 handelt.

Hier ist der Code:

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

Sie haben vielleicht vermutet, dass dieser Test fehlschlagen würde, weil wir keinen Produktionsserver haben, der 5 Produkte an unsere Front-End-Anwendung zurückgibt. Also, was machen wir? Wir verspotten den Server in Mirage! Wenn wir Mirage einbringen, kann es in unseren Tests alle Netzwerkanrufe abfangen. Lassen Sie uns dies unten tun und den Mirage-Server vor jedem Test in der Funktion beforeEach und ihn auch in der Funktion afterEach herunterfahren. Die Funktionen beforeEach und afterEach werden beide von Cypress bereitgestellt und wurden zur Verfügung gestellt, damit Sie Code vor und nach jedem Testlauf in Ihrer Testsuite ausführen können – daher der Name. Sehen wir uns also den Code dafür an:

 // 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) })

Okay, wir kommen irgendwo hin; Wir haben den Server von Mirage importiert und starten und fahren ihn in den Funktionen beforeEach bzw. afterEach herunter. Lassen Sie uns unsere Produktressource verspotten.

 // 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); });

Hinweis : Sie können jederzeit einen Blick auf die vorherigen Teile dieser Serie werfen, wenn Sie die Mirage-Bits des obigen Codeausschnitts nicht verstehen.

  • Teil 1: Verständnis von Mirage JS-Modellen und -Verknüpfungen
  • Teil 2: Factories, Fixtures und Serializer verstehen
  • Teil 3: Timing, Reaktion und Passthrough verstehen

Okay, wir haben damit begonnen, unsere Serverinstanz zu konkretisieren, indem wir das Produktmodell und auch den Routenhandler für die /api/products -Route erstellt haben. Wenn wir unsere Tests jedoch ausführen, schlagen sie fehl, da wir noch keine Produkte in der Mirage-Datenbank haben.

Lassen Sie uns die Mirage-Datenbank mit einigen Produkten füllen. Um dies zu tun, hätten wir die Methode create() auf unserer Serverinstanz verwenden können, aber das Erstellen von 5 Produkten von Hand scheint ziemlich mühsam zu sein. Es sollte einen besseren Weg geben.

Ach ja, das gibt es. Lassen Sie uns Fabriken nutzen (wie im zweiten Teil dieser Serie erklärt). Wir müssen unsere Produktfabrik wie folgt erstellen:

 // 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); });

Schließlich verwenden wir createList() , um schnell die 5 Produkte zu erstellen, die unser Test bestehen muss.

Lass uns das machen:

 // 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); });

Wenn wir also unseren Test durchführen, besteht er!

Hinweis : Nach jedem Test wird der Server von Mirage heruntergefahren und zurückgesetzt, sodass nichts von diesem Zustand über Tests hinweg durchsickern wird.

Vermeidung mehrerer Mirage-Server

Wenn Sie diese Serie verfolgt haben, werden Sie bemerken, dass wir Mirage in der Entwicklung verwendet haben, um unsere Netzwerkanfragen abzufangen; Wir hatten eine server.js -Datei im Stammverzeichnis unserer App, in der wir Mirage eingerichtet haben. Im Geiste von DRY (Don't Repeat Yourself) denke ich, dass es gut wäre, diese Serverinstanz zu verwenden, anstatt zwei separate Instanzen von Mirage sowohl für die Entwicklung als auch für das Testen zu haben. Erstellen Sie dazu (falls Sie noch keine server.js -Datei haben) einfach eine in Ihrem Projekt- src -Verzeichnis.

Hinweis : Ihre Struktur unterscheidet sich, wenn Sie ein JavaScript-Framework verwenden, aber die allgemeine Idee ist, die Datei server.js im src-Stammverzeichnis Ihres Projekts einzurichten.

Mit dieser neuen Struktur exportieren wir also eine Funktion in server.js , die für die Erstellung unserer Mirage-Serverinstanz verantwortlich ist. Lass uns das tun:

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

Schließen wir die Implementierung der makeServer Funktion ab, indem wir den Mirage JS-Server entfernen, den wir in homepage.test.js erstellt haben, und ihn dem makeServer Funktionskörper hinzufügen:

 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; }

Jetzt müssen Sie nur noch makeServer in Ihren Test importieren. Die Verwendung einer einzelnen Mirage-Serverinstanz ist sauberer; Auf diese Weise müssen Sie nicht zwei Serverinstanzen für Entwicklungs- und Testumgebungen verwalten.

Nach dem Import der Funktion makeServer sollte unser Test nun so aussehen:

 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); });

Wir haben jetzt also einen zentralen Mirage-Server, der uns sowohl beim Entwickeln als auch beim Testen dient. Sie können auch die Funktion makeServer verwenden, um Mirage in der Entwicklung zu starten (siehe erster Teil dieser Serie).

Ihr Mirage-Code sollte keinen Weg in die Produktion finden. Daher müssen Sie Mirage je nach Build-Setup nur im Entwicklungsmodus starten.

Hinweis : Lesen Sie meinen Artikel zum Einrichten von API-Mocking mit Mirage und Vue.js, um zu sehen, wie ich das in Vue gemacht habe, damit Sie in jedem von Ihnen verwendeten Front-End-Framework replizieren können.

Testumgebung

Mirage hat zwei Umgebungen: Entwicklung (Standard) und Test . Im Entwicklungsmodus hat der Mirage-Server eine Standardantwortzeit von 400 ms (die Sie anpassen können. Siehe dazu den dritten Artikel dieser Serie), protokolliert alle Serverantworten an die Konsole und lädt die Entwicklungsstartwerte.

In der Testumgebung haben wir jedoch:

  • 0 Verzögerungen, um unsere Tests schnell zu halten
  • Mirage unterdrückt alle Protokolle, um Ihre CI-Protokolle nicht zu verschmutzen
  • Mirage ignoriert auch die seeds() Funktion, sodass Ihre Seed-Daten ausschließlich für die Entwicklung verwendet werden können, aber nicht in Ihre Tests eindringen. Dies trägt dazu bei, dass Ihre Tests deterministisch bleiben.

Lassen Sie uns unseren makeServer aktualisieren, damit wir die Vorteile der Testumgebung nutzen können. Dazu lassen wir es ein Objekt mit der Umgebungsoption akzeptieren (wir werden es standardmäßig auf Entwicklung setzen und es in unserem Test überschreiben). Unsere server.js sollte nun so aussehen:

 // 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; }

Beachten Sie auch, dass wir die Umgebungsoption mithilfe der ES6-Eigenschaftskürzel an die Mirage-Serverinstanz übergeben. Jetzt, wo dies vorhanden ist, aktualisieren wir unseren Test, um den zu testenden Umgebungswert zu überschreiben. Unser Test sieht nun so aus:

 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); });

AAA-Tests

Mirage empfiehlt einen Teststandard, der als Triple-A- oder AAA-Testansatz bezeichnet wird. Dies steht für Arrange , Act und Assert . Diesen Aufbau konnten Sie bereits in unserem obigen Test erkennen:

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

Möglicherweise müssen Sie dieses Muster durchbrechen, aber in 9 von 10 Fällen sollte es für Ihre Tests gut funktionieren.

Lassen Sie uns Fehler testen

Bisher haben wir unsere Homepage getestet, um zu sehen, ob sie 5 Produkte enthält, aber was ist, wenn der Server ausgefallen ist oder etwas beim Abrufen der Produkte schief gelaufen ist? Wir müssen nicht warten, bis der Server heruntergefahren ist, um daran zu arbeiten, wie unsere Benutzeroberfläche in einem solchen Fall aussehen würde. Wir können dieses Szenario einfach mit Mirage simulieren.

Lassen Sie uns einen 500 (Serverfehler) zurückgeben, wenn sich der Benutzer auf der Startseite befindet. Wie wir in einem früheren Artikel gesehen haben, verwenden wir zum Anpassen von Mirage-Antworten die Response-Klasse. Lassen Sie uns es importieren und unseren Test schreiben.

 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"); });

Was für eine Welt der Flexibilität! Wir überschreiben einfach die Antwort, die Mirage zurückgeben würde, um zu testen, wie unsere Benutzeroberfläche angezeigt wird, wenn das Abrufen von Produkten fehlschlägt. Unsere gesamte homepage.test.js -Datei würde nun so aussehen:

 // 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"); });

Beachten Sie, dass die Änderung, die wir am /api/products -Handler vorgenommen haben, nur in unserem Test lebt. Das heißt, es funktioniert so, wie wir es zuvor definiert haben, wenn Sie sich im Entwicklungsmodus befinden.

Wenn wir also unsere Tests durchführen, sollten beide bestehen.

Hinweis : Ich glaube, dass es erwähnenswert ist, dass die Elemente, nach denen wir in Cypress fragen, in Ihrer Front-End-Benutzeroberfläche vorhanden sein sollten. Cypress erstellt keine HTML-Elemente für Sie.

Testen der Produktdetailseite

Lassen Sie uns abschließend die Benutzeroberfläche der Produktdetailseite testen. Das testen wir also:

  • Benutzer können den Produktnamen auf der Produktdetailseite sehen

Lasst uns anfangen. Zuerst erstellen wir einen neuen Test, um diesen Benutzerfluss zu testen.

Hier ist der Test:

 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'); });

Ihre homepage.test.js sollte schließlich so aussehen.

 // 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'); });

Wenn Sie Ihre Tests durchführen, sollten alle drei bestanden werden.

Einpacken

Es hat Spaß gemacht, Ihnen in dieser Serie das Innere von Mirage JS zu zeigen. Ich hoffe, Sie waren besser gerüstet, um eine bessere Front-End-Entwicklungserfahrung zu haben, indem Sie Mirage verwenden, um Ihren Back-End-Server zu verspotten. Ich hoffe auch, dass Sie das Wissen aus diesem Artikel nutzen, um mehr Akzeptanz-/UI-/End-to-End-Tests für Ihre Front-End-Anwendungen zu schreiben.

  • Teil 1: Verständnis von Mirage JS-Modellen und -Verknüpfungen
  • Teil 2: Factories, Fixtures und Serializer verstehen
  • Teil 3: Timing, Reaktion und Passthrough verstehen
  • Teil 4: Verwenden von Mirage JS und Cypress für UI-Tests