Перевод: Valentino Gagliardi — 4 ways to fake an API in frontend development
Это руководство предполагает базовое понимание теории тестирования и опыт работы с тестовыми фреймворками.
Примеры в статье написаны на чистом JavaScript.
Чтобы поэкспериментировать с примерами, у вас должна быть рабочая установка Node.js в вашей системе.
Чтобы сразу перейти к тренировкам, прыгайте сюда, иначе сначала рассмотрим немного теории!
Тесты и программные компоненты, которые мы хотим протестировать, в большинстве случаев имеют зависимости. Типичной зависимостью может быть, например, внешний источник данных.
Представьте фрагмент кода для получения данных из RESTFUL API и, в частности, его ответ — это всегда дополнительная зависимость для нашего кода.
Получение и отображение данных — один из наиболее распространенных вариантов использования фронтендовского кода. Обычно это делается путем обращения к внешнему RESTFUL API, который содержит для нас некоторый JSON. При этом нужно учитывать что не правильно вызывать настоящее продуктовое API в тестовой среде или в процессе разработке. Есть ряд причин не делать этого.
Во-первых, тесты не должны зависеть от внешних сервисов. Тест с использованием реального API может завершиться неудачно, если:
С другой стороны, вы сами не захотите использовать продуктовое API в разработке потому, что:
Кроме того, чаще всего фронтенд-разработчики нуждаются и хотят разрабатывать независимо от бекенд составляющей, например потому что RESTFUL API может быть не готово на 100%.
Именно здесь в игру вступают mocking и stubbing.
В разработке программного обеспечения mocking (фиктивный, ложный) и stubbing (заглушка) — два термина, тесно связанные с тестированием.
Важно отметить, что понятие «фейки» в тестировании и в разработке никоим образом не ново. Есть еще одно определение этих «фейков»: тестовые двойники.
В контексте типичного внешнего интерфейса mocking и stubbing помогают «подделать» реальное API, заменив его сфабрикованным сервисом или «хирургической» подменой одной функции.
Это особенно полезно при работе с независимыми интерфейсными архитектурами, которые чаще всего взаимодействуют с удаленными API, и не всегда находящимися под нашим прямым контролем.
В чем разница между mocking и stubbing?
Как видите, разница между mocking и stubbing не так очевидна.
Рассмотрим этот момент подробнее:
Mocking направлен на замену так называемых исходящих зависимостей наших тестов: обычно это сетевые вызовы. Например, мы можем имитировать Fetch или XMLHttpRequest и заменить фактическую функцию нашей собственной версией.
Stubbing — это метод замены сетевых ответов.
Большинство инструментов mocking/stubbing работают путем перехвата и замены функций Fetch или XMLHttpRequest, таким образом чтобы предоставить поддельный ответ. В более строгом смысле они всегда имитируют исходящие сетевые функции.
Другая категория инструментов, такая как json-server, — это полноценный stubs (заглушка), потому что они не затрагивают тестируемый код.
У обоих методов есть свои варианты использования. В этой статье я рассмотрю наиболее распространенные подходы к mocking и stubbing в контексте веб-приложения.
Для начала клонируйте этот репозитарий минимальная среда разработки:
git clone https://github.com/valentinogagliardi/stubbing-mocking.git cd stubbing-mocking
Внутри установите зависимости:
npm i
Откройте проект в своем любимом редакторе.
Начнем этот тур с первой библиотеки группы: Mirage JS. Основанный на другой библиотеке, Pretender, он предлагает интересный подход к mocking API.
Чтобы установить библиотеку в свой проект, выполните:
npm i miragejs --save-dev
Мы хотим перехватить и имитировать ответ API в процессе разработки или тестирования.
Предположим, у нас есть часть интерфейса, где пользователь щелкает и запускает вызов Fetch. Чтобы продолжить, создайте новый HTML-файл в src/index.html со следующим содержимым:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Mocking and stubbing in frontend development</title> </head> <body> <div> <button id="fetch-btn">FETCH</button> </div> </body> </html>
В другой файл, src/index.js пропишите следующую логику:
/* Получить данные при нажатии кнопки */const button = document.getElementById("fetch-btn"); button.addEventListener("click", function() { fetch("/api/users/") .then(response => { if (!response.ok) throw Error(response.statusText); return response.json(); }) .then(json => buildList(json)); }); /* Создаем список пользователей */function buildList(data) { console.log(data); }
Примечание: файл должен быть в точности в src/index.js, потому что он будет загружаться webpack.
Чтобы перехватить вызов /api/users/ с помощью Mirage JS, мы можем использовать объект Server, настроенный с поддельной конечной точкой:
import { Server } from "miragejs"; new Server({ routes() { this.namespace = "api"; this.get("/users/", () => { return [ { name: "Angy", surname: "T." }, { name: "Chris", surname: "B." }, { name: "Juliana", surname: "Crain" } ]; }); } });
Этот код может находиться в том же файле src/index.js для простоты или в другом файле, импортированном в проект.
Вот полный код src/index.js:
import { Server } from "miragejs"; new Server({ routes() { this.namespace = "api"; this.get("/users/", () => { return [ { name: "Angy", surname: "T." }, { name: "Chris", surname: "B." }, { name: "Juliana", surname: "Crain" } ]; }); } }); const button = document.getElementById("fetch-btn"); button.addEventListener("click", function() { fetch("/api/users/") .then(response => { if (!response.ok) throw Error(response.statusText); return response.json(); }) .then(json => buildList(json)); }); function buildList(data) { console.log(data); }
Теперь запустите проект с помощью команды:
npm start
Откройте консоль браузера и нажмите кнопку. Вы должны увидеть в консоли следующий вывод:
Как видите, Mirage JS перехватывает запрос и выдает ложный ответ. Конфигурация довольно проста. Мы можем определить пространство имен для API:
this.namespace = "api";
Мы так же можем определять маршруты (он принимает все методы HTTP):
this.get("/users/", () => { // return a response });
Таким образом, при переходе к /api/users/ Mirage JS выполняет подделку ответа. И это лишь краткий обзор того, что умеет Mirage JS.
Mirage JS даже имеет свое ORM, с поддержкой моделей и отношений между ними. Я рекомендую вам почитать документацию для более сложных примеров.
Поскольку Mirage JS потенциально может перехватывать все сетевые запросы, вы должны быть осторожны, импортируя его в процессе разработки.
В проектах на основе webpack (Reat, Vue) вы можете положиться на process.env.NODE_ENV для динамического импорта и организации загрузки библиотеки только в процессе разработки и тестирования:
const loadMirage = () => import("miragejs"); export function loadMirageInDev() { if (process.env.NODE_ENV === "development") { loadMirage().then(({ Server }) => { return new Server({ routes() { this.namespace = baseURL; this.get("/users/", () => { return [/* your stuff here */]; }); this.get("/articles/", () => { return [/* your stuff here */]; }); }, }); }); } }
Эта логика должна находиться в отдельном файле, который вы импортируете в основную точку входа.
Просто имейте в виду, что если вы вызываете API при монтировании компонента, например created во Vue, или componentDidMount/useEffect в React, вам необходимо загрузать Mirage JS до визуализации оболочки приложения.
Другой вариант — импортировать Mirage JS только на стадии тестирования.
Это mocking или stubbing? Поскольку Mirage JS заменяет Fetch и XMLHttpRequest для предоставления ложного ответа, то мы можем сказать, что это mocking. Но для нас это прозрачно, потому что в нашем коде нам не нужно напрямую изменять Fetch или XMLHttpRequest.
MSW — это еще один инструмент, который попадает в ту же категорию, что и Mirage JS, но работает в Service Worker. Посмотрите это для сравнения.
Jest — это средство запуска тестов JavaScript, то есть библиотека JavaScript для создания, выполнения и структурирования тестов.
Jest — один из самых популярных средств запуска тестов в наши дни и выбор по умолчанию для многих проектов.
Мы хотим перехватить и имитировать ответ API при модульном тестировании.
Чтобы установить библиотеку в свой проект, выполните команду:
npm i jest --save-dev
Давайте также настроим сценарий NPM для запуска наших тестов из командной строки.
Откройте package.json и настройте сценарий под названием «test» для запуска Jest:
"scripts": { "test": "jest" },
Поскольку мы запускаем Jest из командной строки через Node.js, который не включает Fetch API, нам нужно установить полифилл для Node:
npm i whatwg-fetch --save-dev
По умолчанию Jest ожидает найти тестовые файлы в папке с именем __tests__ в папке вашего проекта.
Создайте новую папку с этим именем и внутри нее создайте файл APITest.spec.js. Через мгновение мы запустим первый тест.
В этом тесте мы собираемся протестировать функцию, которая вызывает /api/users/ удаленного API. Мы хотим проверить, что функция возвращает массив пользователей из API:
const { getUsers } = require("../src/common/usersAPI"); beforeAll(() => { require("whatwg-fetch"); }); describe("Users API", () => { test("it returns an array of users", async () => { const expected = [ { name: "Jonas", surname: "T." }, { name: "Chris", surname: "B." }, { name: "Juliana", surname: "Crain" }, { name: "Caty", surname: "B." } ]; const json = await getUsers(); expect(json).toMatchObject(expected); }); });
Здесь мы импортируем getUsers из внешнего модуля (мы собираемся создать его через минуту). Затем мы проверяем, возвращает ли getUsers ожидаемый ответ.
С помощью beforeAll мы загружаем полифилл Fetch, whatwg-fetch, чтобы сделать наш тест максимально реалистичным. (Это не всегда необходимо, особенно с приложениями вроде create-react-app и т.п.).
Перед запуском теста создайте новый файл в src/common/usersAPI.js со следующим кодом:
const ENDPOINT = "https://api.valentinog.com/api/users/"; function getUsers() { return fetch(ENDPOINT) .then(response => { if (!response.ok) throw Error(response.statusText); return response.json(); }) .then(json => json); } module.exports = { getUsers };
Теперь запустите тест с помощью команды:
npm test
Вы должны увидеть следующие ошибки:
TypeError: Network request failed // and Error: Cross origin http://localhost forbidden
Я ожидаю этого, потому что URL, по которому мы обращаемся, еще не существует на сервере, поэтому бэкэнд не может установить заголовок CORS. Подумайте о реалистичном сценарии, в котором вам нужно разработать пользовательский интерфейс, но бэкэнд еще не готов на 100%.
Эта ошибка — хорошая возможность имитировать (mock) Fetch, чтобы мы могли заменить window.fetch поддельной версией.
Во-первых, в тесте мы используем метод Jest spyOn для перехвата метода:
jest.spyOn(window, "fetch").mockImplementation(() => { // TODO });
Затем мы предоставим поддельную версию ответа Fetch:
jest.spyOn(window, "fetch").mockImplementation(() => { const fetchResponse = { ok: true, json: () => Promise.resolve(expected) }; return Promise.resolve(fetchResponse); });
Вот полный тест:
const { getUsers } = require("../src/common/usersAPI"); beforeAll(() => { require("whatwg-fetch"); }); describe("Users API", () => { test("it returns an array of users", async () => { const expected = [ { name: "Jonas", surname: "T." }, { name: "Chris", surname: "B." }, { name: "Juliana", surname: "Crain" }, { name: "Caty", surname: "B." } ]; jest.spyOn(window, "fetch").mockImplementation(() => { const fetchResponse = { ok: true, json: () => Promise.resolve(expected) }; return Promise.resolve(fetchResponse); }); const json = await getUsers(); expect(json).toMatchObject(expected); }); });
В конце каждого теста, если необходимо, восстановите реальную версию Fetch с помощью mockRestore():
const { getUsers } = require("../src/common/usersAPI"); beforeAll(() => { require("whatwg-fetch"); }); describe("Users API", () => { test("it returns an array of users", async () => { // пропущено .... // возвращаем исходное состояние window.fetch.mockRestore(); }); });
Таким образом, вы не будете мешать другим тестам.
Теперь запустите тест, и теперь он должен пройти успешно:
npm test
Mocking — это не только замена функций подделками, но и слежка за вызовами функций.
Ранее мы видели, что mocking — это также когда вы оцениваете фиктивную функцию, чтобы увидеть:
В Jest мы можем сделать подобное утверждение для mocked функции следующим образом:
beforeAll(() => { require("whatwg-fetch"); }); describe("Users API", () => { test("it returns an array of users", async () => { // пропущено... expect(window.fetch).toHaveBeenCalledWith( "https://api.valentinog.com/api/users/" ); expect(window.fetch).toHaveBeenCalledTimes(1); expect(json).toMatchObject(expected); window.fetch.mockRestore(); }); });
Вот что мы проверили:
Это mocking или stubbing? Это правильный mocking, потому что мы намеренно заменяем Fetch нашей собственной версией, чтобы изменить ответ и подтвердить его вызов.
Cypress — это инструмент для выполнения функциональных (также называемых сквозными) тестов. В отличие от таких средств запуска тестов, как Jest, которые работают на уровне модулей, Cypress запускает настоящий браузер для проведения тестов.
(Недавно Cypress также получил возможность запускать модульные тесты, но это уже другая история).
К сожалению, на сегодняшний день Cypress все еще не может перехватить Fetch, но работа над добавлением его поддержки продолжается. Начиная с версии 4.9.0 Cypress имеет экспериментальную поддержку Fetch stubbing. Чтобы включить его, настройте experimentalFetchPolyfill в cypress.json:
{ "experimentalFetchPolyfill": true }
В этом разделе мы увидим, как заменить (stub) ответ от XMLHttpRequest.
Мы хотим перехватить и заменить ответ API при функциональном тестировании.
Чтобы установить Cypress в свой проект, выполните команду:
npm i cypress --save-dev
После установки запустите тест первый раз с помощью команды:
node_modules/.bin/cypress open
В вашем проекте появится куча новых папок. Вы можете безопасно удалить папку с примером example.
Давайте также настроим сценарий NPM для запуска наших тестов из командной строки.
Откройте package.json и настройте скрипт с именем «e2e» для запуска Cypress:
"scripts": { "e2e": "cypress open" },
По умолчанию Cypress ожидает найти тестовые файлы в папке cypress/integration вашего проекта.
Создайте тест в файле cypress/integration/APITest.spec.js. Через мгновение мы запустим свой первый тест Cypress.
Структура теста Cypress напоминает то, что мы видели с Jest:
describe("Users API", () => { it("should return an array of users", () => { // }); });
У нас есть функции describe и it и они имеет то же значение, что и тестовых блоках Jest.
В описание действия в функциональном тесте вместо того, чтобы сказать «должен возвращать массив пользователей — should return an array of users», мы можем сказать «должен увидеть список пользователей — should see a list of users», потому что нам нужно выдать себя за реального пользователя, использующего браузер. Поэтому наш тест будет выглядеть так:
describe("Users API", () => { it("should see a list of users", () => { // }); });
Теперь представьте, что мы хотим подделать ответ для той же конечной точки «/api/users/«.
В Cypress мы используем функцию cy.server для отслеживания сетевых запросов, и cy.route для настройки поддельной конечной точки API:
describe("Users API", () => { it("should see a list of users", () => { cy.visit("http://localhost:8080/"); cy.server(); cy.route({ url: "/api/users/", method: "GET", response: [ { name: "Juliana", surname: "Crain" }, { name: "Molly", surname: "F." } ] }); cy.contains("FETCH").click(); }); });
Чтобы попробовать этот тест, убедитесь, что src/index.html все еще на месте со следующим содержимым:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Mocking and stubbing in frontend development</title> </head> <body> <div> <button id="fetch-btn">FETCH</button> </div> </body> </html>
В src/index.js нам нужно изменить логику, чтобы использовать XMLHttpRequest вместо Fetch:
const button = document.getElementById("fetch-btn"); button.addEventListener("click", function() { // AJAX запрос с XMLHttpRequest const request = new XMLHttpRequest(); request.open("GET", "/api/users/"); request.onload = function() { const jsonResponse = JSON.parse(this.response); buildList(jsonResponse); }; request.send(); }); function buildList(data) { console.log(data); }
Теперь запустите тест в терминале:
npm start
В другом терминале запустите Cypress:
npm run e2e
Вы увидите всплывающее окно, выберите тест, который мы написали минуту назад, и убедитесь, что тест прошел:
Вы можете увидеть XHR STUB как подтверждение stubbing (заглушки).
В этом тесте мы просто заменили ответ, но вы можете легко создать список с помощью JavaScript и добавить тест для проверки списка HTML в DOM.
Это mocking или stubbing? В отличие от Mirage JS или spyOn от Jest, Cypress обертывает и наблюдает за XMLHttpRequest, чтобы при необходимости предоставить ложный ответ, он не имитирует каждый вызов. Но когда мы используем cy.route, Cypress должен заменить XMLHttpRequest на поддельную версию. Поэтому, несмотря на то, что в документации они определяют этот подход как stubbing, технически это все еще mock.
В будущем Cypress будет использовать прозрачный прокси-слой для stubbing сети.
Последний инструмент этого тура — json-server, это еще один пакет NPM для создания stubbing API. json-server — это HTTP-сервер, который может прослушивать локальную (или удаленную) сеть.
Мы хотим перехватить и заменить ответ от API при разработке и тестировании.
Чтобы установить json-сервер в свой проект используйте следующую команду:
npm i json-server --save-dev
Затем создайте новый файл с именем db.json в папке проекта. Этот файл будет содержать поддельную базу данных для нашего сервера. В нем может быть что-то в этом роде:
{ "users": [ { "name": "Jonas", "surname": "T." }, { "name": "Chris", "surname": "B." }, { "name": "Juliana", "surname": "Crain" }, { "name": "Caty", "surname": "B." } ] }
Теперь откройте package.json и настройте скрипт с именем «stubapi» для запуска json-сервера:
"scripts": { "stubapi": "json-server db.json", "test": "jest", "e2e": "cypress open" },
Для запуска сервера выполните команду:
npm run stubapi
В консоли вы должны увидеть следующий вывод:
Resources http://localhost:3000/users Home http://localhost:3000
Это означает, что теперь вы можете получить доступ к http://localhost:3000/users, чтобы получить список поддельных пользователей.
Как видите, эта вся эта структура URLов — это первая проблема с json-server. Нелегко получить что-то вроде http://localhost:3000/api/users, не засорив конфигурацию дополнительным кодом.
json-server требует собственный список маршрутов и ответов, и в большинстве случаев это усложняет проект.
Теперь, когда установлен поддельный сервер, мы можем тестировать как модульные, так и функциональные тесты.
Чтобы протестировать заглушку с помощью Jest, откройте __tests__/APITest.spec.js, очистите все и внесите следующий код:
const { getUsers } = require("../src/common/usersAPI"); beforeAll(() => { require("whatwg-fetch"); }); describe("Users API", () => { test("it returns an array of users", async () => { const expected = [ { name: "Jonas", surname: "T." }, { name: "Chris", surname: "B." }, { name: "Juliana", surname: "Crain" }, { name: "Caty", surname: "B." } ]; const json = await getUsers("http://localhost:3000/users"); expect(json).toMatchObject(expected); }); });
В этом тесте нам больше не нужно имитировать (mock), мы просто вызываем API-заглушку.
Теперь откройте src/common/usersAPI.js и настройте функцию, чтобы она принимала следующий параметр:
function getUsers(endpoint) { return fetch(endpoint) .then(response => { if (!response.ok) throw Error(response.statusText); return response.json(); }) .then(json => json); } module.exports = { getUsers };
Теперь запустите в терминале:
npm run stubapi
В другом терминале запустите модульные тесты:
npm test
Тест должен пройти:
PASS __tests__/APITest.spec.js Users API ✓ it returns an array of users (3 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.962 s, estimated 1 s Ran all test suites.
Теперь посмотрим на функциональный тест.
Этот тест тоже простой. Откройте integration/APITest.spec.js, очистите все и внесите следующий код:
describe("Users API", () => { it("should see a list of users", () => { cy.visit("http://localhost:8080/"); cy.contains("FETCH").click(); cy.contains(/Jonas|Chris|Juliana/); }); });
В src/index.js не забудьте вызвать API-заглушку:
const button = document.getElementById("fetch-btn"); button.addEventListener("click", function() { // AJAX request with XMLHttpRequest const request = new XMLHttpRequest(); request.open("GET", "http://localhost:3000/users/"); request.onload = function() { const jsonResponse = JSON.parse(this.response); buildList(jsonResponse); }; request.send(); }); function buildList(data) { const ul = document.createElement("ul"); for (const user of data) { const li = document.createElement("li"); li.innerText = user.name; ul.appendChild(li); } document.body.appendChild(ul); }
Теперь, когда json-сервер все еще запущен, запустите приложение в терминале:
npm start
В другом терминале запустите Cypress:
npm run e2e
Вы увидите всплывающее окно, выберите тест, который мы написали минуту назад, и убедитесь, что тест прошел:
Это mocking или stubbing? json-server — типичный пример stubbing, потому что мы предоставляем фальшивую внешнюю службу нашим тестам, не затрагивая ничего в нашем коде (кроме небольшой корректировки URL-адреса в нашем примере).
Неправильно жестко кодировать URL-адрес API в вызовах Fetch или XMLHttpRequest, особенно при использовании такого инструмента, как json-server.
Обычной практикой является использование другого URL-адреса в производственной среде и среде разработки (или тестировании).
Создайте файл с именем .env.development в папке вашего проекта и настройте переменную среды с префиксом REACT_APP_:
REACT_APP_BASE_URL=http://localhost:3000
Затем в вашем коде получите доступ к переменной следующим образом:
fetch(`${process.env.REACT_APP_BASE_URL}/users/`) .then(/* разместите ваш код сюда */) .then(/* разместите ваш код сюда */) .catch(/* здесь должен быть обработчик ошибок */);
Для производственной среды вместо этого создайте файл с именем .env.production в папке проекта и настройте переменную среды с префиксом REACT_APP_:
REACT_APP_BASE_URL=https://api.production.io
Ваш вызов Fetch остается прежним:
fetch(`${process.env.REACT_APP_BASE_URL}/users/`) .then(/* разместите ваш код сюда */) .then(/* разместите ваш код сюда */) .catch(/* здесь должен быть обработчик ошибок */);
Создайте файл с именем .env.development в папке проекта и настройте переменную среды с префиксом VUE_APP_:
VUE_APP_BASE_URL=http://localhost:3000
Затем в вашем коде получите доступ к переменной следующим образом:
fetch(`${process.env.VUE_APP_BASE_URL}/users/`) .then(/* разместите ваш код сюда */) .then(/* разместите ваш код сюда */) .catch(/* здесь должен быть обработчик ошибок */);
Для производственной среды вместо этого создайте файл с именем .env.production в папке проекта и настройте переменную среды с префиксом VUE_APP_:
VUE_APP_BASE_URL=https://api.production.io
Ваш вызов Fetch остается прежним:
fetch(`${process.env.VUE_APP_BASE_URL}/users/`) .then(/* разместите ваш код сюда */) .then(/* разместите ваш код сюда */) .catch(/* здесь должен быть обработчик ошибок */);
Здесь вы можете спросить, какой из всех этих инструментов для mocking и stubbing мне следует использовать?
Все зависит от ваших потребностей! Вот краткий обзор плюсов и минусов для принятия обоснованного решения.
json-server
Плюсы:
Минусы:
Cypress stubbing
Плюсы:
Минусы:
Jest mocking
Плюсы:
Минусы:
Mirage JS
Плюсы:
Минусы: Посмотрите, как он сравнивается с другими инструментами.
Есть что добавить? Напишите автору статьи!
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…