Содержание:
title
В прошлой статье было продемонстрировано использование Mock Service Worker (MSW) на практике с несколькими советами по его использованию в production-проектах. В этой статье мы рассмотрим, как использовать генераторы данных без MSW в тестах и как интегрировать MSW в 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),
};
}
Обновлённый генератор использует оператор ??
(nullish coalescing, оператор нулевого слияния), который возвращает правый операнд, только если левый равен 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);
});
});