Введение: Почему тесты без моков — как кофе без кофеина?
Представьте: вы пишете приложение на TypeScript, которое отправляет email, общается с базой данных и ходит во внешнее API. Вы запускаете тесты — и вдруг падает всё, потому что почтовый сервер временно недоступен. Знакомо?
Вот тут на сцену выходят mock-объекты (заглушки) и фикстуры (тестовые данные). Они позволяют изолировать код от внешнего мира и сделать тесты быстрыми, надёжными и предсказуемыми. В этой статье на русском мы на реальных примерах разберём, как правильно использовать mock в TypeScript, какие бывают фикстуры для TypeScript и как не наломать дров.
Статья написана для PythonLib — образовательного портала, где мы учим писать качественный код. Поехали!
Что такое mock-объекты и фикстуры?
Mock-объекты: актёры-дублёры в вашем коде
Mock-объект (или просто mock) — это замена реального объекта, которая имитирует его поведение. Вы говорите: "Когда вызовут метод sendEmail(), верни true". И тест не лезет в настоящую почту.
В TypeScript моки чаще всего создаются с помощью библиотек Jest или Vitest. Они позволяют проверить, что метод был вызван, с какими аргументами и сколько раз.
Фикстуры: подготовленные данные для тестов
Фикстуры (fixtures) — это заранее подготовленные наборы данных. Например, объект пользователя, список заказов или конфигурация приложения. Вместо того чтобы каждый раз создавать их вручную, вы храните фикстуры в отдельных файлах.
Связка mock + fixture — это стандарт тестирования TypeScript в 2024 году.
Практика: пишем тесты с моками и фикстурами
Настройка окружения
Для примеров будем использовать Jest (хотя всё то же самое работает и в Vitest). Убедитесь, что у вас установлены зависимости:
npm install --save-dev jest @types/jest ts-jest
Добавьте в jest.config.js:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
Пример 1: Mock сервиса отправки email
Допустим, у нас есть сервис уведомлений:
// notificationService.ts
export class EmailService {
async sendEmail(to: string, subject: string, body: string): Promise<boolean> {
// Реальная отправка — не будем её трогать
return true;
}
}
export class NotificationManager {
constructor(private emailService: EmailService) {}
async notifyUser(email: string, message: string): Promise<string> {
const result = await this.emailService.sendEmail(email, 'Уведомление', message);
return result ? 'Отправлено' : 'Ошибка';
}
}
Теперь напишем тест с mock-объектом:
// notificationService.test.ts
import { NotificationManager, EmailService } from './notificationService';
describe('NotificationManager', () => {
it('должен отправить уведомление и вернуть "Отправлено"', async () => {
// Создаём mock
const mockEmailService: jest.Mocked<EmailService> = {
sendEmail: jest.fn().mockResolvedValue(true),
};
const manager = new NotificationManager(mockEmailService);
const result = await manager.notifyUser('test@test.com', 'Привет!');
expect(result).toBe('Отправлено');
expect(mockEmailService.sendEmail).toHaveBeenCalledWith(
'test@test.com',
'Уведомление',
'Привет!'
);
});
it('должен вернуть "Ошибка", если отправка не удалась', async () => {
const mockEmailService: jest.Mocked<EmailService> = {
sendEmail: jest.fn().mockResolvedValue(false),
};
const manager = new NotificationManager(mockEmailService);
const result = await manager.notifyUser('test@test.com', 'Ошибка!');
expect(result).toBe('Ошибка');
});
});
Обратите внимание: мы использовали jest.Mocked<EmailService> — это типизированный mock в TypeScript. Без него TypeScript ругался бы на несуществующие методы.
Пример 2: Фикстуры для тестовых данных
Создадим папку __fixtures__ и файл с тестовыми данными:
// __fixtures__/user.fixture.ts
export interface UserFixture {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
export const createUserFixture = (overrides?: Partial<UserFixture>): UserFixture => ({
id: 1,
name: 'Иван Петров',
email: 'ivan@example.com',
role: 'user',
...overrides,
});
Теперь в тесте:
import { createUserFixture } from './__fixtures__/user.fixture';
describe('User Service', () => {
it('должен создать пользователя с ролью admin', () => {
const adminUser = createUserFixture({ role: 'admin' });
expect(adminUser.role).toBe('admin');
expect(adminUser.name).toBe('Иван Петров'); // остальные поля по умолчанию
});
});
Такой подход с фикстурами TypeScript делает тесты чистыми и переиспользуемыми. Вы можете переопределять только нужные поля через overrides.
Продвинутые техники: шпионы, заглушки и fake-объекты
Spy (шпион) — следим за реальным объектом
Иногда нужно проверить, что метод реального объекта был вызван, но при этом не менять его поведение. Для этого используем jest.spyOn:
const emailService = new EmailService();
const spy = jest.spyOn(emailService, 'sendEmail').mockResolvedValue(true);
const manager = new NotificationManager(emailService);
await manager.notifyUser('a@b.com', 'test');
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore(); // обязательно восстановить!
Stub (заглушка) — фиксированный ответ
Заглушка — это простейший mock, который всегда возвращает одно и то же. В Jest это jest.fn().mockReturnValue(...):
const stub = jest.fn().mockReturnValue(42);
expect(stub()).toBe(42);
expect(stub()).toBe(42); // всегда 42
Fake (подделка) — упрощённая реализация
Fake — это полноценная, но упрощённая версия сервиса. Например, вместо базы данных — массив в памяти:
class FakeDatabase {
private data: any[] = [];
async save(item: any): Promise<void> { this.data.push(item); }
async findAll(): Promise<any[]> { return this.data; }
}
Fake-объекты особенно полезны в интеграционных тестах