Мок-объекты и фикстуры в TypeScript тестах: от новичка до профи

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему тесты без моков — как кофе без кофеина?

Представьте: вы пишете приложение на 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-объекты особенно полезны в интеграционных тестах

Блоги

Рекомендаций по книгам