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