ts-mockito для TypeScript: полное руководство с примерами

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

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

Начать курс

ts-mockito для TypeScript: полное руководство с примерами

1. Что это и зачем нужна

ts-mockito — это мощная библиотека для создания моков (mock), стабов (stub) и шпионов (spy) в TypeScript. Она вдохновлена популярной Java-библиотекой Mockito и предоставляет знакомый API для разработчиков, переходящих с Java или Python. Основная цель ts-mockito — упростить написание unit-тестов, изолируя тестируемый код от внешних зависимостей: баз данных, HTTP-клиентов, файловых систем и других сервисов.

Зачем это нужно? В современных TypeScript-приложениях классы и функции часто зависят от других компонентов. Без моков тестирование таких связей превращается в интеграционное тестирование, требующее настройки окружения. ts-mockito позволяет создавать "фальшивые" объекты с заранее заданным поведением, проверять, какие методы вызывались и с какими аргументами. Это делает тесты быстрыми, детерминированными и легко поддерживаемыми.

Библиотека поддерживает все основные возможности Mockito: создание моков через mock(), настройку поведения через when() и thenReturn(), верификацию вызовов через verify(), а также работу с шпионами через spy(). ts-mockito полностью типизирована и отлично интегрируется с популярными тестовыми фреймворками, такими как Jest, Mocha и Jasmine.

2. Установка

Установка ts-mockito осуществляется через npm (Node Package Manager). Выполните следующую команду в корне вашего проекта:

npm install ts-mockito --save-dev

Если вы используете yarn:

yarn add ts-mockito --dev

Библиотека не имеет обязательных зависимостей, кроме самого TypeScript (версия 4.0+). После установки вы можете импортировать необходимые функции в тестовых файлах:

import { mock, when, verify, instance, anything, anyOfClass, deepEqual, spy } from 'ts-mockito';

Обратите внимание: ts-mockito использует прокси-объекты для перехвата вызовов. Это означает, что вы всегда работаете с "обёрнутыми" объектами, а не с реальными экземплярами классов.

3. Быстрый старт — минимальный рабочий пример

Рассмотрим простейший пример: у нас есть класс UserService, который зависит от UserRepository. Мы хотим протестировать метод getUser, не обращаясь к реальной базе данных.

// user-repository.ts
export class UserRepository {
    async findById(id: number): Promise<{ id: number; name: string } | null> {
        // Реальный запрос к БД
        return null;
    }
}

// user-service.ts
export class UserService {
    constructor(private repository: UserRepository) {}

    async getUser(id: number): Promise<string> {
        const user = await this.repository.findById(id);
        if (!user) {
            throw new Error('User not found');
        }
        return `User: ${user.name}`;
    }
}

// user-service.spec.ts
import { mock, when, instance, verify, anything } from 'ts-mockito';
import { UserService } from './user-service';
import { UserRepository } from './user-repository';

describe('UserService', () => {
    it('should return user name when user exists', async () => {
        // 1. Создаём мок репозитория
        const mockRepository: UserRepository = mock(UserRepository);
        
        // 2. Настраиваем поведение: при вызове findById(1) возвращаем объект
        when(mockRepository.findById(1)).thenResolve({ id: 1, name: 'Alice' });
        
        // 3. Получаем реальный экземпляр мока
        const repositoryInstance = instance(mockRepository);
        
        // 4. Создаём сервис с моком
        const service = new UserService(repositoryInstance);
        
        // 5. Выполняем тестируемый метод
        const result = await service.getUser(1);
        
        // 6. Проверяем результат
        expect(result).toBe('User: Alice');
        
        // 7. Верифицируем, что метод был вызван с правильным аргументом
        verify(mockRepository.findById(1)).once();
    });
});

Этот пример демонстрирует базовый паттерн: mock() → when() → instance() → тест → verify(). Все последующие примеры будут строиться на этой основе.

4. Основные методы/функции

Ниже приведены 8 ключевых функций ts-mockito с подробным описанием и примерами.

4.1. mock<T>(class: new (...args: any[]) => T): T

Что делает: Создаёт мок-объект для указанного класса. Все методы мока по умолчанию возвращают undefined (или пустой Promise для асинхронных методов).

import { mock, instance } from 'ts-mockito';

class Calculator {
    add(a: number, b: number): number {
        return a + b;
    }
}

const mockCalc = mock(Calculator);
const calcInstance = instance(mockCalc);

// По умолчанию метод add вернёт undefined
console.log(calcInstance.add(2, 3)); // undefined

4.2. when(mock: T): MethodStubVerificator<T>

Что делает: Начинает цепочку настройки поведения для мока. После вызова when() вы указываете метод и аргументы, а затем вызываете thenReturn(), thenResolve() или thenThrow().

import { mock, when, instance } from 'ts-mockito';

const mockCalc = mock(Calculator);
when(mockCalc.add(1, 2)).thenReturn(100);

const calc = instance(mockCalc);
console.log(calc.add(1, 2)); // 100
console.log(calc.add(3, 4)); // undefined (не настроено)

4.3. instance(mock: T): T

Что делает: Преобразует мок-объект (прокси) в реальный экземпляр, который можно передавать в тестируемый код. Без вызова instance() мок нельзя использовать как обычный объект.

const mockRepo = mock(UserRepository);
const repoInstance = instance(mockRepo);
// repoInstance — это настоящий объект, который можно передать в конструктор

4.4. verify(mock: T): MethodStubVerificator<T>

Что делает: Проверяет, что определённый метод мока был вызван с указанными аргументами. Можно дополнительно указать количество вызовов: .once(), .twice(), .times(n) или .never().

import { mock, verify, instance } from 'ts-mockito';

const mockCalc = mock(Calculator);
const calc = instance(mockCalc);

calc.add(1, 2);
calc.add(1, 2);

verify(mockCalc.add(1, 2)).twice(); // проходит
verify(mockCalc.add(1, 2)).once();  // не проходит — было два вызова

4.5. anything(): ArgumentMatcher

Что делает: Сопоставляется с любым аргументом. Используется, когда точное значение аргумента не важно для настройки или верификации.

import { mock, when, verify, anything, instance } from 'ts-mockito';

const mockCalc = mock(Calculator);
when(mockCalc.add(anything(), 5)).thenReturn(42);

const calc = instance(mockCalc);
console.log(calc.add(100, 5)); // 42
console.log(calc.add(-7, 5));  // 42

verify(mockCalc.add(anything(), 5)).twice();

4.6. anyOfClass(classType: new (...args: any[]) => T): ArgumentMatcher

Что делает: Сопоставляется с любым экземпляром указанного класса. Полезно, когда метод принимает объекты определённого типа, а точное значение не важно.

class User {
    constructor(public id: number, public name: string) {}
}

class UserProcessor {
    process(user: User): string {
        return `Processed ${user.name}`;
    }
}

const mockProcessor = mock(UserProcessor);
when(mockProcessor.process(anyOfClass(User))).thenReturn('Mocked');

const processor = instance(mockProcessor);
console.log(processor.process(new User(1, 'Bob'))); // 'Mocked'

4.7. deepEqual(expected: any): ArgumentMatcher

Что делает: Выполняет глубокое сравнение объектов (по содержимому, а не по ссылке). Используется, когда нужно сопоставить аргумент-объект с определённой структурой.

import { mock, when, deepEqual, instance } from 'ts-mockito';

interface Config {
    host: string;
    port: number;
}

class Connector {
    connect(config: Config): string {
        return `Connected to ${config.host}:${config.port}`;
    }
}

const mockConnector = mock(Connector);
when(mockConnector.connect(deepEqual({ host: 'localhost', port: 8080 }))).thenReturn('Mocked connection');

const connector = instance(mockConnector);
console.log(connector.connect({ host: 'localhost', port: 8080 })); // 'Mocked connection'
console.log(connector.connect({ host: 'example.com', port: 3000 })); // undefined

4.8. spy(realObject: T): T

Что делает: Создаёт шпион (spy) вокруг реального объекта. В отличие от мока, шпион по умолчанию вызывает реальные методы, но позволяет переопределить некоторые из них и верифицировать вызовы.

import { spy, when, verify, instance } from 'ts-mockito';

class RealService {
    greet(name: string): string {
        return `Hello, ${name}!`;
    }
    farewell(name: string): string {
        return `Goodbye, ${name}!`;
    }
}

const realService = new RealService();
const spyService = spy(realService);

// Переопределяем только метод greet
when(spyService.greet('Alice')).thenReturn('Hi, Alice!');

const serviceInstance = instance(spyService);
console.log(serviceInstance.greet('Alice'));   // 'Hi, Alice!' (переопределено)
console.log(serviceInstance.farewell('Bob'));  // 'Goodbye, Bob!' (реальный вызов)

verify(spyService.greet('Alice')).once();

5. Реальный практический пример

Рассмотрим более сложный сценарий: у нас есть сервис для обработки заказов OrderService, который зависит от PaymentGateway, EmailService и OrderRepository. Мы протестируем полный процесс оформления заказа.

// ---- domain/models.ts ----
export interface Order {
    id: number;
    userId: number;
    total: number;
    status: 'pending' | 'paid' | 'shipped';
}

export interface PaymentResult {
    success: boolean;
    transactionId?: string;
    error?: string;
}

// ---- services/payment-gateway.ts ----
export class PaymentGateway {
    async processPayment(userId: number, amount: number): Promise<PaymentResult> {
        // Реальный вызов к платежному шлюзу
        return { success: true, transactionId: 'txn_123' };
    }
}

// ---- services/email-service.ts ----
export class EmailService {
    async sendEmail(to: string, subject: string, body: string): Promise<void> {
        // Реальная отправка email
        console.log(`Email sent to ${to}: ${subject}`);
    }
}

// ---- repositories/order-repository.ts ----
export class OrderRepository {
    async save(order: Order): Promise<Order> {
        // Сохранение в БД
        return order;
    }
}

// ---- services/order-service.ts ----
export class OrderService {
    constructor(
        private paymentGateway: PaymentGateway,
        private emailService: EmailService,
        private orderRepository: OrderRepository
    ) {}

    async placeOrder(userId: number, items: { price: number }[], userEmail: string): Promise<Order> {
        const total = items.reduce((sum, item) => sum + item.price, 0);
        
        const paymentResult = await this.paymentGateway.processPayment(userId, total);
        if (!paymentResult.success) {
            throw new Error(`Payment failed: ${paymentResult.error}`);
        }

        const order: Order = {
            id: Math.floor(Math.random() * 1000),
            userId,
            total,
            status: 'paid'
        };

        const savedOrder = await this.orderRepository.save(order);
        
        await this.emailService.sendEmail(
            userEmail,
            'Order Confirmation',
            `Your order #${savedOrder.id} has been placed successfully. Total: $${total}`
        );

        return savedOrder;
    }
}

// ---- tests/order-service.spec.ts ----
import { mock, when, instance, verify, anything, deepEqual } from 'ts-mockito';
import { OrderService } from '../services/order-service';
import { PaymentGateway } from '../services/payment-gateway';
import { EmailService } from '../services/email-service';
import { OrderRepository } from '../repositories/order-repository';

describe('OrderService', () => {
    let mockPaymentGateway: PaymentGateway;
    let mockEmailService: EmailService;
    let mockOrderRepository: OrderRepository;
    let orderService: OrderService;

    beforeEach(() => {
        // Создаём моки для всех зависимостей
        mockPaymentGateway = mock(PaymentGateway);
        mockEmailService = mock(EmailService);
        mock                    

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