Содержание:
Как использовать Mock Service Worker в тестах?
В прошлой статье было продемонстрировано использование Mock Service Worker (MSW) на практике с несколькими советами по его использованию в production-проектах. В этой статье мы рассмотрим, как использовать генераторы данных без MSW в тестах и как интегрировать service worker в unit и e2e-тесты. В статье будет использоваться код из прошлого урока, поэтому рекомендую ознакомиться с ней.
Используем генераторы данных без MSW в тестах
Помимо работы с Mock Service Worker, созданные генераторы можно применять и в unit-тестах. Однако для этого требуется добавить возможность контроля над генерируемыми данными т.к. в тестах нам важно контролировать данные, которые мы передаём в тестируемую функцию. Это необходимо для проверки базового функционала, граничных случаев и корректности обработки ошибок.
Доработка генераторов данных для unit-тестов
Доработаем наши генераторы, чтобы сделать их более гибкими. Для этого создадим интерфейс с настройками генерации:
type PartialBaseAccount = Partial<Omit<BaseAccountModel, 'id'>>;
export interface DetailAccountArgs {
baseAccount?: PartialBaseAccount;
withFamily?: boolean;
changelogLimit?: number;
}
Кратко про параметры интерфейса:
baseAccountиспользует утилитарные типы TypeScript:Partial<T>делает все поля типа необязательными, что позволяет передавать только те данные, которые нужны для конкретного теста. Остальные поля будут сгенерированы черезfaker.js;Omit<T, K>исключает указанные свойства из типа. В нашем случае удаляемid, который передаётся отдельным аргументом функции. Альтернативой было бы объединение всех параметров в один объект настроек, но это потребовало бы рефакторинга существующего кода;
withFamilyпозволяет явно указать, должна ли модель счёта содержать данные о семье;changelogLimitзадаёт количество записей в истории изменений счёта.
Теперь доработаем генераторы, чтобы они учитывали новые параметры. Начнём с функции создания базового счёта:
export function generateBaseAccount(id?: string, options: PartialBaseAccount = {}): BaseAccountModel {
const now = Date.now();
const dateParams = { from: '2024-10-17', to: now };
const balanceParams = { min: 0.0, max: 3200000.0 };
return {
id: id ?? faker.database.mongodbObjectId(),
createdAt: options.createdAt ?? faker.date.between(dateParams).toISOString(),
updatedAt: options.updatedAt ?? faker.date.between(dateParams).toISOString(),
balance: options.balance ?? faker.number.float(balanceParams).toFixed(2).toString(),
title: options.title ?? faker.finance.accountName(),
description: options.description ?? faker.lorem.text(),
isDeposit: options.isDeposit ?? faker.datatype.boolean(),
currency: options.currency ?? faker.helpers.arrayElement(currencies),
};
}
Обновлённый генератор использует оператор ?? (оператор нулевого слияния), который возвращает правый операнд, только если левый равен null или undefined. Это позволяет передавать в options даже «ложные» значения вроде пустой строки или false.
Аналогично модифицируем генератор детальной информации о счёте:
const generateFamilyForAccount = (withFamily: boolean) => withFamily ? generateBaseFamily() : null;
export function generateDetailAccount(id?: string, options: DetailAccountArgs = {}): DetailAccountModel {
return {
...generateBaseAccount(id, options.baseAccount),
user: generateUser(),
changelog: 'changelogLimit' in options ? generateChangelogList(options.changelogLimit) : generateChangelogList(),
family: 'withFamily' in options
? generateFamilyForAccount(options.withFamily!)
: faker.helpers.arrayElement([generateBaseFamily(), null]),
history: generateBaseTransactionsList(),
};
}
В расширенном генераторе особое внимание стоит обратить на поле family. Если флаг withFamily не указан в опциях, используется случайный выбор через faker.js. Если же флаг передан, то семья либо гарантированно будет добавлена в модель счёта, либо гарантированно будет отсутствовать – это важно для тестирования различных состояний приложения.
Пишем тесты с использованием этих генераторов
На примере тестирования страницы счёта рассмотрим, как использовать наши генераторы на практике. На этой странице есть заголовок, который отображает тип счёта в одном из четырёх вариантов: «Личный депозит», «Личный счёт», «Семейный депозит» или «Семейный счёт».
По частям разберём написанные мною тесты:
Для начала, создаём тип данных для перечисляемого значения возможных кейсов, чтобы потом использовать метод forEach в тестах.
interface AccountTypeTest {
name: string; // Описание теста
config: { // Конфигурация генератора
baseAccount: {
isDeposit: boolean; // Депозит или обычный счёт
};
withFamily: boolean; // Семейный или личный счёт
};
expectedTitle: string; // Ожидаемый заголовок
}
Затем на основе этого типа мы создаём набор тестовых сценариев под каждый из четырёх возможных заголовков:
const accountTypeTests: AccountTypeTest[] = [
{
name: 'семейный счёт',
config: {
baseAccount: { isDeposit: false },
withFamily: true
},
expectedTitle: 'Семейный счёт'
},
// ... остальные сценарии для разных типов счетов
];
И наконец пишем тесты с использованием цикла:
accountTypeTests.forEach(({ name, config, expectedTitle }) => {
it(`отображает ${name}`, async () => {
const mockAccount = generateDetailAccount(1, config);
vi.mocked(AccountsRepository.getAccountDetails).mockResolvedValue(mockAccount);
const wrapper = mount(AccountPage);
await flushPromises();
expect(wrapper.get(`[data-test-id="${testId}"]`).text()).toBe(expectedTitle);
});
});
При таком подходе к unit-тестированию приложения мы имеем то, что:
- Тестовые данные генерируются предсказуемо, но с случайными значениями для несущественных полей;
- Тесты сохраняют свою читабельность и понятность;
- Легко добавлять тестовые сценарии на основе имеющихся генераторов данных.
Как использовать Mock Service Worker в Unit-тестах?
Напрямую использовать MSW в тестах также возможно, но это не всегда оправдано. В unit-тестах мы тестируем отдельные части приложения, а не взаимодействие между ними. Поэтому в большинстве случаев достаточно использовать генераторы данных и мокать только те функции, которые взаимодействуют с внешними сервисами. Но, если вам всё-таки нужно использовать MSW в unit-тестах, то дальше мы поговорим о том, как это сделать.
Как интегрировать MSW в Vitest?
Для того, чтобы использовать MSW в unit-тестах, необходимо создать экземпляр setupServer и запустить его в beforeAll. Пример показан на основе моего pet-проекта “Family Accounting”.
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
import { setupServer } from 'msw/node';
import { handlers } from '@/mocks/handlers';
const server = setupServer(...handlers);
describe('component', () => {
// Запускаем перехватчик запросов перед всеми тестам
beforeAll(() => {
server.listen();
});
// Останавливает перехватчик запросов после всех тестов
afterAll(() => {
server.close();
});
// Сбрасываем перехватчики запросов после каждого теста
afterEach(() => {
server.resetHandlers();
});
it('должен загрузить список аккаунтов', async () => {
const { loading, accountsList, pagination, fetchAccounts } = useAccountsList('user');
await fetchAccounts();
expect(loading.value).toBe(false);
expect(accountsList.value.length).toBe(15);
expect(pagination.value.currentPage).toBe(2);
});
});
Минусы такого подхода:
- Магические числа. В примере кода присутствует жесткое связывание теста с данными из мока — 15 элементов и 2 страницы. Тесты скрыто зависят от имплементации моков, которые даже не импортируются напрямую в тестовый файл. При изменении этих значений в mock-функции тест неизбежно упадет.
- Чрезмерная сложность. Настройка MSW в тесте усложняет его понимание и замедляет выполнение, в то время как тесты должны оставаться простыми и понятными.
- Сложность отладки. Тест может упасть по множеству непрозрачных причин: изменения в перехватчике запросов, модификации генератора данных, правки в глобальном файле настройки перехватчиков, изменения в API MSW после обновления библиотеки.
P.S. Весь код установки MSW можно вынести в отдельный файл vitest-setup.ts и подключить в vitest.config.ts через опцию setupFiles. Это избавит от дублирования кода, но активирует MSW во всех тестах, даже там, где он не нужен. Использовать такой подход не рекомендую.
Как интегрировать MSW в Jest?
Для использования внутри конкретного теста синтаксис практически идентичен Vitest — различий в API нет. Основное отличие появляется при глобальной настройке: в конфигурации Jest (jest.config.js) используется опция setupFilesAfterEnv вместо setupFiles, которую мы видели в Vitest.
Как использовать Mock Service Worker в Playwright?
Как тестировать гонки запросов с помощью MSW?
Преимущества и недостатки MSW в e2e-тестировании
Преимущества:
- Скорость выполнения. MSW исключает обращения к реальному API, что значительно ускоряет прогон тестов;
- Предсказуемость. MSW обеспечивает контроль над получаемыми данными, гарантируя стабильность тестов. При работе с тестовым сервером существует риск падения тестов из-за изменения данных;
- Гибкость. Библиотека позволяет настраивать моки запросов для проверки граничных случаев и корректности обработки ошибок.
- Независимость от бэкенда. Тестирование фронтенда может проводиться до полной готовности API, что ускоряет разработку.
Недостатки:
- Дублирование функционала. Playwright уже содержит встроенные инструменты для перехвата и подмены запросов. Использование MSW оправдано в основном для приложений с SSR, где Playwright не может подменить серверные запросы, поскольку они выполняются до загрузки страницы в браузере и вне контекста тестовой среды;
- Усложнение инфраструктуры. Интеграция MSW добавляет дополнительный слой сложности в процесс e2e-тестирования, что усложняет поддержку тестов.
Когда стоит использовать MSW в e2e-тестах
MSW в e2e-тестировании будет полезен в следующих случаях:
- SSR-приложения — когда запросы выполняются на сервере до рендеринга страницы, а их нужно подменять на моки;
- Тестирование граничных случаев — для симуляции редких ситуаций и ошибок API без обращения к реальному серверу;
- Нестабильный или недоступный API — когда тестовый сервер может быть недоступен или обладает нестабильным набором тестовых данных;
- Разработка в параллель с бэкендом — когда API находится в разработке, но нужно тестировать интерфейс уже сейчас;
- Воспроизведение сложных сценариев работы с API — когда требуется протестировать последовательность зависимых запросов.