Mirage JS Deep Dive: Używanie Mirage JS i Cypress do testowania interfejsu użytkownika (część 4)
Opublikowany: 2022-03-10Jeden z moich ulubionych cytatów o testowaniu oprogramowania pochodzi z dokumentacji Fluttera. To mówi:
„Jak możesz zapewnić, że Twoja aplikacja będzie nadal działać, gdy dodasz więcej funkcji lub zmienisz istniejące funkcje? Pisząc testy.”
W związku z tym ta ostatnia część serii Mirage JS Deep Dive skupi się na użyciu Mirage do testowania aplikacji front-endowych JavaScript.
Uwaga : w tym artykule założono środowisko Cypress. Cypress to platforma testowa do testowania interfejsu użytkownika. Możesz jednak przenieść wiedzę tutaj do dowolnego używanego środowiska testowego UI lub frameworka.
Przeczytaj poprzednie części serii:
- Część 1: Zrozumienie modeli i skojarzeń Mirage JS
- Część 2: Zrozumienie fabryk, urządzeń i serializatorów
- Część 3: Zrozumienie czasu, odpowiedzi i przekazywania
Podkład do testów interfejsu użytkownika
Test interfejsu użytkownika lub interfejsu użytkownika to forma testów akceptacyjnych wykonywana w celu zweryfikowania przepływów użytkownika w aplikacji front-endowej. Nacisk w tego rodzaju testach oprogramowania jest kładziony na użytkownika końcowego, który jest rzeczywistą osobą, która będzie wchodzić w interakcję z Twoją aplikacją internetową na różnych urządzeniach, od komputerów stacjonarnych, laptopów po urządzenia mobilne. Ci użytkownicy będą łączyć się lub wchodzić w interakcje z Twoją aplikacją za pomocą urządzeń wejściowych, takich jak klawiatura, mysz lub ekrany dotykowe. Dlatego testy interfejsu użytkownika są napisane tak, aby naśladować interakcję użytkownika z Twoją aplikacją tak blisko, jak to możliwe.
Weźmy na przykład witrynę e-commerce. Typowy scenariusz testu interfejsu użytkownika to:
- Użytkownik może przeglądać listę produktów podczas odwiedzania strony głównej.
Inne scenariusze testów interfejsu użytkownika mogą być następujące:
- Użytkownik może zobaczyć nazwę produktu na stronie szczegółów produktu.
- Użytkownik może kliknąć przycisk „dodaj do koszyka”.
- Użytkownik może dokonać zakupu.
Masz pomysł, prawda?
Wykonując testy interfejsu użytkownika, będziesz głównie polegał na swoich stanach zaplecza, tj. Czy zwrócił produkty, czy wystąpił błąd? Rolą, jaką odgrywa w tym Mirage, jest udostępnienie tych stanów serwera, które można dostosować w razie potrzeby. Więc zamiast wysyłać rzeczywiste żądanie do serwera produkcyjnego w testach interfejsu użytkownika, wysyłasz żądanie do serwera próbnego Mirage.
W dalszej części tego artykułu będziemy przeprowadzać testy interfejsu użytkownika na fikcyjnym interfejsie użytkownika aplikacji internetowej e-commerce. Więc zacznijmy.
Nasz pierwszy test interfejsu użytkownika
Jak wspomniano wcześniej, w tym artykule założono środowisko Cypress. Cypress sprawia, że testowanie interfejsu użytkownika w sieci jest szybkie i łatwe. Możesz symulować kliknięcia i nawigację, a także możesz programowo odwiedzać trasy w swojej aplikacji. Więcej informacji na temat Cypress znajdziesz w dokumentacji.
Tak więc, zakładając, że Cypress i Mirage są dla nas dostępne, zacznijmy od zdefiniowania funkcji proxy dla twojego żądania API. Możemy to zrobić w support/index.js
naszej konfiguracji Cypress. Po prostu wklej następujący kod w:
// 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])) }) }) } })
Następnie, w pliku ładowania aplikacji ( main.js
dla Vue, index.js
dla React), użyjemy Mirage do proxy żądań API Twojej aplikacji do funkcji handleFromCypress
tylko wtedy, gdy Cypress jest uruchomiony. Oto kod do tego:
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) }) }) }, }) }
Dzięki tej konfiguracji, za każdym razem, gdy działa Cypress, Twoja aplikacja wie, że może używać Mirage jako symulowanego serwera dla wszystkich żądań API.
Kontynuujmy pisanie testów interfejsu użytkownika. Zaczniemy od przetestowania naszej strony głównej, aby sprawdzić, czy wyświetla się na niej 5 produktów . Aby to zrobić w Cypress, musimy utworzyć plik homepage.test.js
w folderze tests
w katalogu głównym katalogu projektu. Następnie powiemy Cypressowi, aby wykonał następujące czynności:
- Odwiedź stronę główną tj.
/
trasa - Następnie potwierdź, czy ma elementy li z klasą
product
, a także sprawdza, czy jest ich 5 w liczbach.
Oto kod:
// homepage.test.js it('shows the products', () => { cy.visit('/'); cy.get('li.product').should('have.length', 5); });
Można się domyślić, że ten test się nie powiedzie, ponieważ nie mamy serwera produkcyjnego zwracającego 5 produktów do naszej aplikacji front-endowej. Więc co robimy? Wyśmiewamy serwer w Mirage! Jeśli wprowadzimy Mirage, może przechwycić wszystkie połączenia sieciowe w naszych testach. Zróbmy to poniżej i uruchom serwer Mirage przed każdym testem w funkcji beforeEach
, a także wyłączmy go w funkcji afterEach
. Funkcje beforeEach
i afterEach
są dostarczane przez Cypress i zostały udostępnione, aby można było uruchomić kod przed i po każdym uruchomieniu testu w zestawie testów — stąd nazwa. Zobaczmy więc kod do tego:
// 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) })
Dobra, dokądś idziemy; zaimportowaliśmy serwer z Mirage i uruchamiamy go i wyłączamy odpowiednio w beforeEach
i afterEach
. Przejdźmy do wyśmiewania naszego zasobu produktów.
// 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); });
Uwaga : zawsze możesz rzucić okiem na poprzednie części tej serii, jeśli nie rozumiesz bitów Mirage powyższego fragmentu kodu.
- Część 1: Zrozumienie modeli i skojarzeń Mirage JS
- Część 2: Zrozumienie fabryk, urządzeń i serializatorów
- Część 3: Zrozumienie czasu, odpowiedzi i przekazywania
Okej, zaczęliśmy rozbudowywać naszą instancję Server, tworząc model produktu, a także tworząc procedurę obsługi trasy dla trasy /api/products
. Jeśli jednak uruchomimy nasze testy, zakończy się to niepowodzeniem, ponieważ nie mamy jeszcze żadnych produktów w bazie danych Mirage.
Zapełnijmy bazę danych Mirage niektórymi produktami. W tym celu moglibyśmy użyć metody create()
na naszej instancji serwera, ale ręczne tworzenie 5 produktów wydaje się dość żmudne. Powinien być lepszy sposób.
Ach tak, jest. Wykorzystajmy fabryki (jak wyjaśniono w drugiej części tej serii). Musimy stworzyć naszą fabrykę produktów w następujący sposób:
// 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); });
Następnie, na koniec, użyjemy createList()
, aby szybko utworzyć 5 produktów, które nasz test musi przejść.
Zróbmy to:
// 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); });
Więc kiedy przeprowadzamy nasz test, to przechodzi!
Uwaga : po każdym teście serwer Mirage jest wyłączany i resetowany, więc żaden z tych stanów nie będzie przeciekał między testami.
Unikanie wielu serwerów Mirage
Jeśli śledziłeś tę serię, zauważyłbyś, kiedy używaliśmy Mirage w fazie rozwoju do przechwytywania naszych żądań sieciowych; mieliśmy plik server.js
w katalogu głównym naszej aplikacji, w którym skonfigurowaliśmy Mirage. W duchu DRY (Don't Repeat Yourself), myślę, że dobrze byłoby wykorzystać tę instancję serwera zamiast mieć dwie oddzielne instancje Mirage zarówno do tworzenia, jak i testowania. Aby to zrobić (jeśli nie masz jeszcze pliku server.js
), po prostu utwórz go w katalogu src swojego projektu.
Uwaga : Twoja struktura będzie się różnić, jeśli używasz frameworka JavaScript, ale ogólną ideą jest skonfigurowanie pliku server.js w katalogu głównym src projektu.
Tak więc z tą nową strukturą wyeksportujemy funkcję w server.js
, która jest odpowiedzialna za stworzenie naszej instancji serwera Mirage. Zróbmy to:
// src/server.js export function makeServer() { /* Mirage code goes here */}
Zakończmy implementację funkcji makeServer
, usuwając serwer Mirage JS, który stworzyliśmy w homepage.test.js
i dodając go do treści funkcji 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; }
Teraz wszystko, co musisz zrobić, to zaimportować makeServer
do swojego testu. Używanie pojedynczej instancji Mirage Server jest czystsze; w ten sposób nie musisz utrzymywać dwóch instancji serwera zarówno dla środowisk deweloperskich, jak i testowych.
Po zaimportowaniu funkcji makeServer
nasz test powinien teraz wyglądać tak:
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); });
Mamy więc teraz centralny serwer Mirage, który służy nam zarówno do tworzenia, jak i testowania. Możesz także użyć funkcji makeServer
, aby uruchomić Mirage w fazie rozwoju (zobacz pierwszą część tej serii).
Twój kod Mirage nie powinien trafić do produkcji. Dlatego, w zależności od konfiguracji kompilacji, musisz uruchomić Mirage tylko w trybie programistycznym.
Uwaga : Przeczytaj mój artykuł na temat konfigurowania API Mocking za pomocą Mirage i Vue.js, aby zobaczyć, jak zrobiłem to w Vue, abyś mógł replikować w dowolnym frameworku front-end, którego używasz.
Środowisko testowe
Mirage ma dwa środowiska: programistyczne (domyślne) i testowe . W trybie deweloperskim serwer Mirage będzie miał domyślny czas odpowiedzi 400 ms (który można dostosować. Zobacz trzeci artykuł z tej serii), rejestruje wszystkie odpowiedzi serwera w konsoli i ładuje nasiona rozwoju.
Natomiast w środowisku testowym mamy:
- 0 opóźnień, aby nasze testy były szybkie
- Mirage blokuje wszystkie dzienniki, aby nie zanieczyszczać dzienników CI
- Mirage zignoruje również funkcję seed
seeds()
, dzięki czemu dane nasion mogą być używane wyłącznie do programowania, ale nie będą wyciekać do testów. Pomaga to w utrzymaniu deterministycznych testów.
Zaktualizujmy nasz makeServer
, abyśmy mogli czerpać korzyści ze środowiska testowego. Aby to zrobić, sprawimy, że zaakceptuje obiekt z opcją środowiska (domyślnie ustawimy go na development i nadpiszemy w naszym teście). Nasz server.js
powinien teraz wyglądać tak:
// 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; }
Należy również zauważyć, że przekazujemy opcję środowiska do instancji serwera Mirage za pomocą skrótu właściwości ES6. Teraz, gdy to już istnieje, zaktualizujmy nasz test, aby zastąpić wartość środowiska do przetestowania. Nasz test wygląda teraz tak:
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); });
Testy AAA
Mirage zachęca do stosowania standardu testowania zwanego podejściem testowym triple-A lub AAA. To oznacza Rozmieść , Działaj i Potwierdź . Możesz zobaczyć tę strukturę już w naszym powyższym teście:
it("shows all the products", function () { // ARRANGE server.createList("product", 5) // ACT cy.visit("/") // ASSERT cy.get("li.product").should("have.length", 5) })
Być może będziesz musiał przełamać ten wzorzec, ale 9 razy na 10 powinien on działać dobrze w twoich testach.
Przetestujmy błędy
Do tej pory przetestowaliśmy naszą stronę główną, aby sprawdzić, czy zawiera 5 produktów, jednak co jeśli serwer nie działa lub coś poszło nie tak z pobieraniem produktów? Nie musimy czekać, aż serwer się wyłączy, aby pracować nad tym, jak nasz interfejs będzie wyglądał w takim przypadku. Możemy po prostu zasymulować ten scenariusz za pomocą Mirage.
Zwróćmy 500 (błąd serwera), gdy użytkownik jest na stronie głównej. Jak widzieliśmy w poprzednim artykule, aby dostosować odpowiedzi Mirage, używamy klasy Response. Zaimportujmy to i napiszmy nasz 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"); });
Co za świat elastyczności! Po prostu nadpisujemy odpowiedź zwróconą przez Mirage, aby przetestować, jak wyświetlałby się nasz interfejs użytkownika, gdyby nie udało się pobrać produktów. Nasz ogólny plik homepage.test.js
wyglądałby teraz tak:
// 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"); });
Zauważ, że modyfikacja, którą wprowadziliśmy do obsługi /api/products
występuje tylko w naszym teście. Oznacza to, że działa tak, jak wcześniej zdefiniowaliśmy, gdy jesteś w trybie programistycznym.
Więc kiedy przeprowadzamy nasze testy, oba powinny przejść.
Uwaga : uważam, że warto zauważyć, że elementy, o które pytamy w Cypressie, powinny istnieć w interfejsie użytkownika. Cypress nie tworzy dla Ciebie elementów HTML.
Testowanie strony szczegółów produktu
Na koniec przetestujmy interfejs użytkownika strony szczegółów produktu. Więc to jest to, co testujemy:
- Użytkownik może zobaczyć nazwę produktu na stronie szczegółów produktu
Weźmy się za to. Najpierw tworzymy nowy test, aby przetestować ten przepływ użytkownika.
Oto 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'); });
Twoja homepage.test.js
powinna wreszcie wyglądać tak.
// 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'); });
Kiedy przeprowadzasz testy, wszystkie trzy powinny zdać.
Zawijanie
Fajnie było pokazać wam w tej serii wnętrze Mirage JS. Mam nadzieję, że zostałeś lepiej przygotowany, aby zacząć tworzyć lepsze środowisko programistyczne, używając Mirage do wyśmiewania swojego serwera zaplecza. Mam również nadzieję, że wykorzystasz wiedzę z tego artykułu, aby napisać więcej testów akceptacji/UI/end-to-end dla swoich aplikacji front-endowych.
- Część 1: Zrozumienie modeli i skojarzeń Mirage JS
- Część 2: Zrozumienie fabryk, urządzeń i serializatorów
- Część 3: Zrozumienie czasu, odpowiedzi i przekazywania
- Część 4: Używanie Mirage JS i Cypress do testowania interfejsu użytkownika