Вступление
Jest - популярная среда тестирования с открытым исходным кодом для JavaScript. Мы можем использовать Jest для создания имитаций в нашем тесте - объектов, которые заменяют реальные объекты в нашем коде во время его тестирования.
В нашей предыдущей серии, посвященной методам модульного тестирования с использованием Sinon.js , мы рассмотрели, как мы можем использовать Sinon.js для заглушки, шпионажа и имитации приложений Node.js, особенно HTTP-вызовов.
В этой серии мы рассмотрим методы модульного тестирования в Node.js с использованием Jest . Jest был создан Facebook и хорошо интегрируется со многими библиотеками и фреймворками JavaScript, такими как React, Angular и Vue, и это лишь некоторые из них. Особое внимание уделяется простоте и производительности.
В этой статье мы рассмотрим, что такое имитаторы, а затем сосредоточимся на том, как настроить Jest для приложения Node.js, чтобы имитировать HTTP-вызов в нашем тесте. Затем мы сравним, как мы используем Jest и Sinon для создания макетов для наших программ.
Что такое моки?
В модульном тестировании макеты предоставляют нам возможность заглушить функциональность, предоставляемую зависимостью, и средства наблюдения за тем, как наш код взаимодействует с зависимостью. Моки особенно полезны, когда включать зависимость напрямую в наши тесты дорого или непрактично, например, в случаях, когда ваш код выполняет HTTP-вызовы API или взаимодействует с уровнем базы данных.
Желательно исключить ответы для этих зависимостей, убедившись, что они вызываются по мере необходимости. Вот тут-то и пригодятся mocks.
Давайте теперь посмотрим, как мы можем использовать Jest для создания макетов в Node.js.
Настройка Jest в приложении Node.js
В этом руководстве мы настроим приложение Node.js, которое будет выполнять HTTP-вызовы JSON API, содержащего фотографии в альбоме. Jest будет использоваться для имитации вызовов API в наших тестах.
Во-первых, давайте создадим каталог, в котором будут находиться наши файлы, и переместимся в него:
$ mkdir PhotoAlbumJest && cd PhotoAlbumJest
Затем давайте инициализируем проект Node с настройками по умолчанию:
$ npm init -y
После инициализации проекта мы приступим к созданию index.js
в корне
каталога:
$ touch index.js
Чтобы помочь нам с HTTP-запросами, мы будем использовать Axios .
Настройка Axios
Мы будем использовать axios
качестве нашего HTTP-клиента. Axios - это
легкий HTTP-клиент на основе обещаний для Node.js, который также может
использоваться веб-браузерами. Это делает его подходящим для нашего
случая использования.
Давайте сначала установим его:
$ npm i axios --save
Перед использованием axios
мы создадим файл с именем axiosConfig.js
помощью которого мы настроим клиент Axios. Настройка клиента позволяет
нам использовать общие настройки для набора HTTP-запросов.
Например, мы можем установить заголовки авторизации для набора HTTP-запросов или, как правило, базовый URL-адрес, который будет использоваться для всех HTTP-запросов.
Создадим файл конфигурации:
touch axiosConfig.js
Теперь давайте обратимся к axios
и настроим базовый URL:
const axios = require('axios');
const axiosInstance = axios.default.create({
baseURL: 'https://jsonplaceholder.typicode.com/albums'
});
module.exports = axiosInstance;
После установки baseURL
мы экспортировали axios
чтобы использовать
его в нашем приложении. Мы будем использовать
www.jsonplaceholder.typicode.com
который представляет собой поддельный
онлайн-API REST для тестирования и создания прототипов.
В index.js
который мы создали ранее, давайте определим функцию,
которая возвращает список URL-адресов фотографий с учетом идентификатора
альбома:
const axios = require('./axiosConfig');
const getPhotosByAlbumId = async (id) => {
const result = await axios.request({
method: 'get',
url: `/${id}/photos?_limit=3`
});
const { data } = result;
return data;
};
module.exports = getPhotosByAlbumId;
Чтобы попасть в наш API, мы просто используем метод axios.request()
нашего экземпляра axios
Мы передаем имя метода, который в нашем случае
является get
и url
который мы будем вызывать.
Строка, которую мы передаем в url
будет объединена с baseURL
из
axiosConfig.js
.
Теперь давайте настроим Jest-тест для этой функции.
Настройка Jest
Чтобы настроить Jest, мы должны сначала установить Jest как зависимость
разработки с помощью npm
:
$ npm i jest -D
-D
- это ярлык для --save-dev
, который указывает NPM сохранить его
как зависимость разработки.
Затем мы приступим к созданию файла конфигурации для Jest с именем
jest.config.js
:
touch jest.config.js
Теперь в jest.config.js
мы установим каталоги, в которых будут
находиться наши тесты:
module.exports = {
testMatch: [
'<rootDir>/**/__tests__/**/?(*.)(spec|test).js',
'<rootDir>/**/?(*.)(spec|test).js'
],
testEnvironment: 'node',
};
Значение testMatch
- это массив глобальных шаблонов, которые Jest
будет использовать для обнаружения тестовых файлов. В нашем случае мы
указываем, что любой файл внутри __tests__
или в любом месте нашего
проекта, имеющий либо .spec.js
либо. test.js
следует рассматривать
как тестовый файл.
Примечание . В JavaScript тестовые файлы .spec.js
заканчиваются на
.spec.js. Разработчики используют слово «спецификация» как сокращение
от «спецификация» . Подразумевается, что тесты содержат функциональные
требования или спецификации для реализуемых функций.
Значение testEnvironment
представляет среду, в которой работает Jest,
то есть в Node.js или в браузере. Вы можете узнать больше о других
допустимых параметрах конфигурации
здесь .
Теперь давайте изменим наш package.json
так, чтобы он использовал Jest
в качестве нашего средства выполнения тестов:
"scripts": {
"test": "jest"
},
Наша установка сделана. Чтобы проверить, что наша конфигурация работает,
создайте тестовый файл в корне каталога с именем index.spec.js
:
touch index.spec.js
Теперь внутри файла напишем тест:
describe('sum of 2 numbers', () => {
it(' 2 + 2 equal 4', () => {
expect(2 + 2).toEqual(4)
});
});
Запустите этот код с помощью следующей команды:
$ npm test
У вас должен получиться такой результат:
PASS ./index.spec.js
sum of 2 numbers
✓ 2 + 2 equal 4 (3ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.897s, estimated 1s
Ran all test suites.
Теперь, когда Jest настроен правильно, мы можем приступить к написанию кода для имитации нашего HTTP-вызова.
Имитация HTTP-вызова с помощью Jest
В index.spec.js
мы начнем с нуля, удалив старый код и напишем новый
скрипт, имитирующий HTTP-вызов:
const axios = require('./axiosConfig');
const getPhotosByAlbumId = require('./index');
jest.mock('./axiosConfig', () => {
return {
baseURL: 'https://jsonplaceholder.typicode.com/albums',
request: jest.fn().mockResolvedValue({
data: [
{
albumId: 3,
id: 101,
title: 'incidunt alias vel enim',
url: 'https://via.placeholder.com/600/e743b',
thumbnailUrl: 'https://via.placeholder.com/150/e743b'
},
{
albumId: 3,
id: 102,
title: 'eaque iste corporis tempora vero distinctio consequuntur nisi nesciunt',
url: 'https://via.placeholder.com/600/a393af',
thumbnailUrl: 'https://via.placeholder.com/150/a393af'
},
{
albumId: 3,
id: 103,
title: 'et eius nisi in ut reprehenderit labore eum',
url: 'https://via.placeholder.com/600/35cedf',
thumbnailUrl: 'https://via.placeholder.com/150/35cedf'
}
]
}),
}
});
Здесь мы сначала импортируем наши зависимости, используя синтаксис
require
Поскольку мы не хотим делать никаких реальных сетевых вызовов,
мы вручную создаем макет нашего axiosConfig
используя метод
jest.mock()
. Метод jest.mock()
принимает путь к модулю в качестве
аргумента и необязательную реализацию модуля в качестве заводского
параметра .
Для параметра factory мы указываем, что наш макет, axiosConfig
,
должен возвращать объект, состоящий из baseURL
и request()
.
baseUrl
устанавливается на базовый URL-адрес API. request()
- это
фиктивная функция, возвращающая массив фотографий.
Функция request()
мы здесь определили, заменяет настоящую
axios.request()
. Когда мы вызываем метод request()
, вместо этого
вызывается наш фиктивный метод.
Важно отметить jest.fn()
. Он возвращает новую фиктивную функцию , и
ее реализация определяется в круглых скобках. То, что мы сделали с
помощью функции mockResolvedValue()
- это новая реализация функции
request()
.
Обычно это делается с помощью функции mockImplementation()
, хотя,
поскольку на самом деле мы просто возвращаем data
содержащие наши
результаты, вместо этого мы можем использовать функцию сахара.
mockResolvedValue()
совпадает с
mockImplementation(() => Promise.resolve(value))
.
Имея макет, давайте продолжим и напишем тест:
describe('test getPhotosByAlbumId', () => {
afterEach(() => jest.resetAllMocks());
it('fetches photos by album id', async () => {
const photos = await getPhotosByAlbumId(3);
expect(axios.request).toHaveBeenCalled();
expect(axios.request).toHaveBeenCalledWith({ method: 'get', url: '/3/photos?_limit=3' });
expect(photos.length).toEqual(3);
expect(photos[0].albumId).toEqual(3)
});
});
После каждого тестового примера мы гарантируем, что
jest.resetAllMocks()
вызывается для сброса состояния всех моков.
В нашем тестовом примере мы вызываем getPhotosByAlbumId()
с
идентификатором 3
в качестве аргумента. Затем мы делаем наши
утверждения.
Первое утверждение предполагает, что был axios.request()
, а второе
утверждение проверяет, что метод был вызван с правильными параметрами.
Мы также проверяем, что длина возвращаемого массива равна 3
и что
первый объект массива имеет albumId
альбома 3
.
Давайте запустим наши новые тесты с помощью:
npm test
У нас должен получиться следующий результат:
PASS ./index.spec.js
test getPhotosByAlbumId
✓ fetches photos by album id (7ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.853s, estimated 1s
Ran all test suites.
С этим новым знакомством и опытом давайте проведем быстрое сравнение опыта тестирования с Jest и Sinon, которые также обычно используются для имитации.
Sinon Mocks против Jest Mocks
Sinon.js и Jest по-разному подходят к концепции издевательства. Ниже приведены некоторые из основных отличий, на которые следует обратить внимание:
- В Jest модули Node.js автоматически имитируются в ваших тестах,
когда вы помещаете фиктивные файлы в
__mocks__
которая находится рядом с папкойnode_modules
Например, если у вас есть файл с именем__mock__/fs.js
, то каждый раз, когдаfs
, Jest будет автоматически использовать макеты. С другой стороны, с Sinon.js вы должны вручнуюsinon.mock()
для каждого теста, который в этом нуждается. - В Jest мы используем метод
jest.fn().mockImplementation()
для замены реализации фиктивной функции на заглушенный ответ. Хороший пример этого можно найти в документации Jest здесь . В Sinon.js мы используем методmock.expects()
чтобы справиться с этим. - Jest предоставляет большое количество методов для работы с их фиктивным API и, в частности, с модулями. Вы можете просмотреть их все здесь . Sinon.js, с другой стороны, имеет меньше методов для работы с макетами и предоставляет в целом более простой API.
- Sinon.js имитирует корабль как часть библиотеки Sinon.js, которую можно подключить и использовать в сочетании с другими фреймворками тестирования, такими как Mocha, и библиотеками утверждений, такими как Chai. С другой стороны, Jest имитирует корабль как часть фреймворка Jest, который также поставляется с собственным API утверждений.
Моки Sinon.js часто наиболее полезны, когда вы тестируете небольшое приложение, которое может не требовать всей мощи такого фреймворка, как Jest. Это также полезно, когда у вас уже есть тестовая установка и вам нужно добавить имитацию к нескольким компонентам в вашем приложении.
Однако при работе с большими приложениями, которые имеют много зависимостей, использование мощности фиктивного API Jest вместе с его фреймворком может быть очень полезным для обеспечения единообразия тестирования.
Заключение
В этой статье мы рассмотрели, как с помощью Jest имитировать HTTP-вызов,
сделанный с помощью axios
. Сначала мы настроили приложение для
использования axios
качестве нашей библиотеки HTTP-запросов, а затем
настроили Jest, чтобы помочь нам с модульным тестированием. Наконец, мы
рассмотрели некоторые различия между Sinon.js и Jest mocks и выяснили,
когда лучше всего использовать их.
Чтобы узнать больше о моках Jest и о том, как их можно использовать для более сложных случаев использования, ознакомьтесь с их документацией здесь .
Как всегда, код из этого руководства можно найти на GitHub .