Mock-объекты и фикстуры в JavaScript: ваш тестовый арсенал
Представьте, что вы пишете модуль, который отправляет запросы к внешнему API погоды. Вы запускаете тесты — и они падают, потому что API временно недоступен. Или хуже: вы тестируете платежную систему, и каждый прогон списывает реальные деньги с карты. Знакомо? Добро пожаловать в мир, где mock-объекты и фикстуры в JavaScript становятся вашими лучшими друзьями.
В этой статье мы не просто перечислим определения. Мы разберем, как с помощью моков, стабов и фикстур сделать тесты быстрыми, надежными и независимыми от внешнего мира. Вы узнаете, как обмануть таймеры, подменить HTTP-запросы и подготовить идеальные тестовые данные. Поехали!
Зачем нужны mock и фикстуры? Боль и спасение
Когда вы пишете юнит-тесты на JavaScript, вы хотите проверить только один модуль. Но реальный код часто зависит от баз данных, файловой системы, сетевых вызовов или случайных чисел. Без моков каждый тест — это лотерея: он может упасть из-за тайм-аута сети или из-за того, что сервер вернул не те данные.
Mock-объекты — это заменители реальных зависимостей. Они имитируют поведение оригинальных объектов, но вы полностью контролируете их ответы. Фикстуры — это заранее подготовленные наборы данных (например, JSON с ответом API), которые вы подставляете в тесты. Вместе они дают суперсилу: вы тестируете логику, а не инфраструктуру.
Mock vs Stub vs Spy: разбираемся в терминах
Перед тем как писать код, давайте наведем порядок в терминах. В экосистеме JavaScript (особенно в Jest и Mocha) эти понятия иногда смешивают, но различия важны:
- Mock (мок) — объект, который заменяет реальную зависимость и позволяет задать ожидаемое поведение. Вы говорите: «Когда вызовут метод getUser с id=5, верни объект {name: 'Alice'}».
- Stub (стаб) — упрощенная версия мока. Он просто возвращает фиксированные данные, не проверяя, как его вызвали. Часто используется для замены функций, которые возвращают случайные значения.
- Spy (шпион) — обёртка над реальной функцией. Он не меняет её поведение, но записывает, сколько раз её вызвали, с какими аргументами и что она вернула. Идеально для проверки побочных эффектов.
В большинстве современных фреймворков (Jest, Sinon.js) моки умеют всё: и подменять реализацию, и шпионить. Но понимание разницы поможет писать более осмысленные тесты.
Фикстуры: как подготовить тестовые данные
Фикстуры в тестах JavaScript — это просто данные, которые вы выносите в отдельные файлы или переменные. Зачем это нужно? Во-первых, чтобы не дублировать 50-строчные JSON-объекты в каждом тесте. Во-вторых, чтобы быстро менять сценарии: одна фикстура для успешного ответа, другая — для ошибки.
Пример простой фикстуры для тестирования модуля работы с пользователями:
// fixtures/users.js
export const userAlice = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
role: 'admin'
};
export const userBob = {
id: 2,
name: 'Bob',
email: 'bob@example.com',
role: 'user'
};
export const apiResponseSuccess = {
status: 200,
data: { users: [userAlice, userBob] }
};
export const apiResponseError = {
status: 500,
message: 'Internal Server Error'
};
Теперь в тестах мы просто импортируем эти объекты. Это делает код чище и позволяет легко воспроизводить кейсы: «А что, если сервер вернул 500?».
Mock в Jest: практические примеры
Jest — самый популярный тестовый фреймворк для JavaScript. Он встроен в Create React App, Next.js и многие другие инструменты. Рассмотрим, как создавать mock-объекты в Jest.
Mock функции с jest.fn()
Допустим, у нас есть функция, которая вызывает колбэк после обработки данных:
// utils/processor.js
export function processData(data, callback) {
const result = data.map(item => item * 2);
callback(result);
return result;
}
В тесте мы хотим проверить, что колбэк вызван с правильными аргументами:
// __tests__/processor.test.js
import { processData } from '../utils/processor';
test('processData вызывает колбэк с удвоенными значениями', () => {
const mockCallback = jest.fn();
const input = [1, 2, 3];
processData(input, mockCallback);
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith([2, 4, 6]);
});
jest.fn() создает пустую функцию-заглушку. Мы не знаем, что она делает — нас интересует только факт вызова и аргументы. Это классический spy.
Mock модулей с jest.mock()
А теперь самое мощное. Допустим, ваш модуль импортирует axios и делает запросы к API. Вы не хотите, чтобы тесты реально ходили в сеть. Используем jest.mock():
// services/userService.js
import axios from 'axios';
export async function getUser(id) {
const response = await axios.get(`/api/users/${id}`);
return response.data;
}
// __tests__/userService.test.js
import { getUser } from '../services/userService';
import axios from 'axios';
jest.mock('axios'); // Автоматически подменяет весь модуль axios
test('getUser возвращает данные пользователя', async () => {
const mockUser = { id: 1, name: 'Alice' };
axios.get.mockResolvedValue({ data: mockUser });
const result = await getUser(1);
expect(axios.get).toHaveBeenCalledWith('/api/users/1');
expect(result).toEqual(mockUser);
});
Обратите внимание: мы не писали никакой реальной реализации. Jest подменил axios так, что все его методы (get, post, etc.) стали моками. Мы просто сказали: «Когда вызовут get, верни промис, который резолвится в {data: mockUser}». Тест пройдёт за миллисекунду и не зависит от сети.
Mock таймеров с jest.useFakeTimers()
Функции, которые используют setTimeout или setInterval, — ещё одна головная боль. Как протестировать код, который ждёт 5 секунд? Ответ: с помощью фейковых таймеров.
// utils/delayedMessage.js
export function showDelayedMessage(message, delay, callback) {
setTimeout(() => {
callback(message);
}, delay);
}
// __tests__/delayedMessage.test.js
import { showDelayedMessage } from '../utils/delayedMessage';
jest.useFakeTimers();
test('showDelayedMessage вызывает колбэк через заданное время', () => {