Подробное знакомство с Mirage JS: использование Mirage JS и Cypress для тестирования пользовательского интерфейса (часть 4)
Опубликовано: 2022-03-10Одна из моих любимых цитат о тестировании программного обеспечения взята из документации Flutter. В нем говорится:
«Как вы можете убедиться, что ваше приложение продолжает работать, когда вы добавляете дополнительные функции или изменяете существующую функциональность? Написав тесты».
В связи с этим последняя часть серии «Глубокое погружение» в Mirage JS будет посвящена использованию Mirage для тестирования внешнего интерфейса JavaScript.
Примечание . В этой статье предполагается среда Cypress. Cypress — это тестовая среда для тестирования пользовательского интерфейса. Однако вы можете перенести полученные знания в любую среду тестирования пользовательского интерфейса или фреймворк, который вы используете.
Читайте предыдущие части серии:
- Часть 1: Понимание моделей и ассоциаций Mirage JS
- Часть 2: Понимание фабрик, фикстур и сериализаторов
- Часть 3: Понимание времени, отклика и прохождения
Учебник по тестированию пользовательского интерфейса
Тест пользовательского интерфейса или пользовательского интерфейса — это форма приемочного тестирования, проводимого для проверки пользовательских потоков вашего внешнего приложения. Акцент в этих видах тестирования программного обеспечения делается на конечном пользователе, то есть на реальном человеке, который будет взаимодействовать с вашим веб-приложением на различных устройствах, от настольных компьютеров, ноутбуков до мобильных устройств. Эти пользователи будут взаимодействовать с вашим приложением, используя устройства ввода, такие как клавиатура, мышь или сенсорные экраны. Поэтому тесты пользовательского интерфейса пишутся так, чтобы максимально точно имитировать взаимодействие пользователя с вашим приложением.
Возьмем, к примеру, сайт электронной коммерции. Типичный сценарий тестирования пользовательского интерфейса:
- Пользователь может просмотреть список продуктов при посещении домашней страницы.
Другими сценариями тестирования пользовательского интерфейса могут быть:
- Пользователь может увидеть название продукта на странице сведений о продукте.
- Пользователь может нажать на кнопку «Добавить в корзину».
- Пользователь может оформить заказ.
Вы поняли идею, верно?
При создании UI-тестов вы в основном будете полагаться на свои серверные состояния, т.е. вернули ли они продукты или ошибку? Роль Mirage в этом заключается в том, чтобы сделать эти состояния сервера доступными для настройки по мере необходимости. Таким образом, вместо того, чтобы делать реальный запрос к вашему рабочему серверу в тестах пользовательского интерфейса, вы делаете запрос к фиктивному серверу Mirage.
В оставшейся части этой статьи мы будем выполнять тесты пользовательского интерфейса на вымышленном пользовательском интерфейсе веб-приложения электронной коммерции. Итак, приступим.
Наш первый тест пользовательского интерфейса
Как указывалось ранее, в этой статье предполагается среда Cypress. Cypress делает тестирование пользовательского интерфейса в Интернете быстрым и простым. Вы можете имитировать клики и навигацию, а также программно посещать маршруты в своем приложении. Подробнее о Cypress см. в документации.
Итак, предполагая, что Cypress и Mirage нам доступны, давайте начнем с определения прокси-функции для вашего API-запроса. Мы можем сделать это в файле support/index.js
нашей установки Cypress. Просто вставьте следующий код:
// 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])) }) }) } })
Затем в файле начальной загрузки вашего приложения ( main.js
для Vue, index.js
для React) мы будем использовать Mirage для проксирования запросов API вашего приложения к функции handleFromCypress
только во время работы Cypress. Вот код для этого:
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) }) }) }, }) }
При такой настройке в любое время, когда Cypress запущен, ваше приложение будет использовать Mirage в качестве фиктивного сервера для всех запросов API.
Давайте продолжим писать тесты пользовательского интерфейса. Мы начнем с тестирования нашей домашней страницы, чтобы увидеть, отображается ли на ней 5 продуктов . Для этого в Cypress нам нужно создать файл homepage.test.js
в папке с tests
в корне каталога вашего проекта. Далее мы скажем Cypress сделать следующее:
- Посетите домашнюю страницу т.е.
/
маршрут - Затем подтвердите , есть ли у него элементы li с классом
product
а также проверьте, равны ли они 5 числам.
Вот код:
// homepage.test.js it('shows the products', () => { cy.visit('/'); cy.get('li.product').should('have.length', 5); });
Вы могли догадаться, что этот тест провалится, потому что у нас нет производственного сервера, возвращающего 5 продуктов в наше интерфейсное приложение. Так что же нам делать? Мокаем сервер в Mirage! Если мы подключим Mirage, он сможет перехватывать все сетевые вызовы в наших тестах. Давайте сделаем это ниже и будем запускать сервер Mirage перед каждым тестом в функции beforeEach
, а также выключать его в функции afterEach
. Функции beforeEach
и afterEach
предоставляются Cypress, и они были доступны, чтобы вы могли запускать код до и после каждого запуска теста в вашем наборе тестов — отсюда и название. Итак, давайте посмотрим код для этого:
// 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) })
Хорошо, мы кое-что получили; мы импортировали сервер из Mirage и запускаем и выключаем его в beforeEach
и afterEach
соответственно. Давайте попробуем издеваться над нашим ресурсом продукта.
// 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); });
Примечание . Вы всегда можете взглянуть на предыдущие части этой серии, если не понимаете биты Mirage в приведенном выше фрагменте кода.
- Часть 1: Понимание моделей и ассоциаций Mirage JS
- Часть 2: Понимание фабрик, фикстур и сериализаторов
- Часть 3: Понимание времени, отклика и прохождения
Хорошо, мы начали конкретизировать наш экземпляр Server, создав модель продукта, а также создав обработчик маршрута для маршрута /api/products
. Однако, если мы запустим наши тесты, они потерпят неудачу, потому что у нас еще нет продуктов в базе данных Mirage.
Давайте заполним базу данных Mirage некоторыми продуктами. Чтобы сделать это, мы могли бы использовать метод create()
на нашем экземпляре сервера, но создание 5 продуктов вручную кажется довольно утомительным. Должен быть лучший способ.
Ах да, есть. Давайте воспользуемся фабриками (как объяснялось во второй части этой серии). Нам нужно создать нашу фабрику продуктов следующим образом:
// 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); });
Затем, наконец, мы будем использовать createList()
для быстрого создания 5 продуктов, которые должны пройти наш тест.
Давай сделаем это:
// 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); });
Итак, когда мы запускаем наш тест, он проходит!
Примечание . После каждого теста сервер Mirage выключается и сбрасывается, поэтому никакие данные этого состояния не будут просачиваться между тестами.

Как избежать нескольких серверов Mirage
Если вы следили за этой серией, вы могли заметить, что мы использовали Mirage в разработке для перехвата наших сетевых запросов; у нас был файл server.js
в корне нашего приложения, где мы настроили Mirage. В духе DRY (не повторяйтесь), я думаю, было бы хорошо использовать этот экземпляр сервера вместо двух отдельных экземпляров Mirage как для разработки, так и для тестирования. Для этого (если у вас еще нет файла server.js
), просто создайте его в каталоге src вашего проекта.
Примечание . Ваша структура будет отличаться, если вы используете среду JavaScript, но общая идея заключается в настройке файла server.js в корневом каталоге src вашего проекта.
Итак, с этой новой структурой мы экспортируем функцию в server.js
, которая отвечает за создание нашего экземпляра сервера Mirage. Давайте сделаем это:
// src/server.js export function makeServer() { /* Mirage code goes here */}
Давайте завершим реализацию функции makeServer
, удалив JS-сервер Mirage, который мы создали в homepage.test.js
, и добавив его в тело функции 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; }
Теперь все, что вам нужно сделать, это импортировать makeServer
в свой тест. Использование одного экземпляра Mirage Server чище; таким образом, вам не нужно поддерживать два экземпляра сервера как для среды разработки, так и для среды тестирования.
После импорта функции makeServer
наш тест должен выглядеть так:
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); });
Итак, теперь у нас есть центральный сервер Mirage, который служит нам как при разработке, так и при тестировании. Вы также можете использовать функцию makeServer
для запуска Mirage в разработке (см. первую часть этой серии).
Ваш код Mirage не должен попасть в производство. Поэтому, в зависимости от настроек сборки, вам нужно будет запускать Mirage только в режиме разработки.
Примечание . Прочтите мою статью о том, как настроить API Mocking с помощью Mirage и Vue.js, чтобы узнать, как я сделал это в Vue, чтобы вы могли воспроизвести его в любой используемой интерфейсной среде.
Среда тестирования
Mirage имеет две среды: development (по умолчанию) и test . В режиме разработки сервер Mirage будет иметь время отклика по умолчанию 400 мс (которое вы можете настроить. См. третью статью этой серии), регистрирует все ответы сервера на консоли и загружает семена разработки.
Однако в тестовой среде имеем:
- 0 задержек, чтобы наши тесты были быстрыми
- Mirage подавляет все журналы, чтобы не загрязнять ваши журналы CI
- Mirage также будет игнорировать функцию seed
seeds()
, чтобы ваши начальные данные можно было использовать исключительно для разработки, но они не попали в ваши тесты. Это поможет сохранить детерминированность ваших тестов.
Давайте обновим наш makeServer
, чтобы мы могли воспользоваться преимуществами тестовой среды. Для этого мы заставим его принимать объект с параметром среды (по умолчанию мы установим его для разработки и переопределим его в нашем тесте). Теперь наш server.js
должен выглядеть так:
// 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; }
Также обратите внимание, что мы передаем параметр среды экземпляру сервера Mirage, используя сокращение свойства ES6. Теперь, когда это на месте, давайте обновим наш тест, чтобы переопределить значение среды для тестирования. Теперь наш тест выглядит так:
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); });
ААА-тестирование
Компания Mirage поддерживает стандарт тестирования, называемый подходом тестирования Triple-A или AAA. Это означает Arrange , Act and Assert . Вы уже могли видеть эту структуру в нашем тесте выше:
it("shows all the products", function () { // ARRANGE server.createList("product", 5) // ACT cy.visit("/") // ASSERT cy.get("li.product").should("have.length", 5) })
Возможно, вам придется сломать этот шаблон, но в 9 случаях из 10 он должен работать нормально для ваших тестов.
Давайте проверим ошибки
До сих пор мы тестировали нашу домашнюю страницу, чтобы увидеть, есть ли на ней 5 продуктов, однако что, если сервер не работает или что-то пошло не так с получением продуктов? Нам не нужно ждать, пока сервер отключится, чтобы работать над тем, как будет выглядеть наш пользовательский интерфейс в таком случае. Мы можем просто смоделировать этот сценарий с помощью Mirage.
Давайте вернем 500 (ошибка сервера), когда пользователь находится на домашней странице. Как мы видели в предыдущей статье, для настройки ответов Mirage мы используем класс Response. Давайте импортируем его и напишем наш тест.
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"); });
Какой мир гибкости! Мы просто переопределяем ответ, который вернет Mirage, чтобы проверить, как будет отображаться наш пользовательский интерфейс, если ему не удастся получить продукты. Наш общий файл homepage.test.js
теперь будет выглядеть так:
// 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"); });
Обратите внимание, что модификация, которую мы внесли в обработчик /api/products
существует только в нашем тесте. Это означает, что он работает так, как мы определили ранее, когда вы находитесь в режиме разработки.
Поэтому, когда мы запускаем наши тесты, оба должны пройти.
Примечание . Я считаю, что стоит отметить, что элементы, которые мы запрашиваем в Cypress, должны существовать в вашем пользовательском интерфейсе. Cypress не создает HTML-элементы за вас.
Тестирование страницы сведений о продукте
Наконец, давайте протестируем пользовательский интерфейс страницы сведений о продукте. Итак, вот что мы тестируем:
- Пользователь может увидеть название продукта на странице сведений о продукте.
Давайте приступим к делу. Во-первых, мы создаем новый тест для проверки этого пользовательского потока.
Вот тест:
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
должна наконец выглядеть так.
// 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'); });
Когда вы запускаете свои тесты, все три должны пройти.
Подведение итогов
Было весело показать вам внутренности Mirage JS в этой серии. Я надеюсь, что вы были лучше подготовлены к тому, чтобы получить лучший опыт разработки внешнего интерфейса, используя Mirage для имитации вашего внутреннего сервера. Я также надеюсь, что вы воспользуетесь знаниями из этой статьи, чтобы написать больше приемочных/UI/end-to-end тестов для ваших интерфейсных приложений.
- Часть 1: Понимание моделей и ассоциаций Mirage JS
- Часть 2: Понимание фабрик, фикстур и сериализаторов
- Часть 3: Понимание времени, отклика и прохождения
- Часть 4. Использование Mirage JS и Cypress для тестирования пользовательского интерфейса