Тестирование модулей и классов
Тестирование ES-модулей и классов — unit-проверка экспортируемых функций, публичного интерфейса классов и их взаимодействия с зависимостями через моки и фейки.
Зачем нужно
Классы и модули — основные единицы организации кода. Тесты на уровне публичного интерфейса защищают от регрессий при рефакторинге внутренней реализации. Правило: тестируй интерфейс, не реализацию.
Где используется
- Сервисы, репозитории, утилиты в TypeScript/JavaScript
- Domain-модели с бизнес-логикой
- State managers (store, reducer, selector)
Основной контент
Тестирование ES-модуля
// math.ts
export function add(a: number, b: number): number { return a + b; }
export function multiply(a: number, b: number): number { return a * b; }
export function divide(a: number, b: number): number {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
// math.test.ts
import { add, multiply, divide } from './math';
describe('math utilities', () => {
test('add: складывает два числа', () => expect(add(2, 3)).toBe(5));
test('multiply: умножает', () => expect(multiply(4, 5)).toBe(20));
test('divide: делит', () => expect(divide(10, 2)).toBe(5));
test('divide: выбрасывает ошибку при делении на 0', () => {
expect( => divide(10, 0)).toThrow('Division by zero');
});
});
Тестирование класса
// Stack.ts
export class Stack<T> {
private items: T = ;
push(item: T): void { this.items.push(item); }
pop: T | undefined { return this.items.pop(); }
peek: T | undefined { return this.items[this.items.length - 1]; }
get size: number { return this.items.length; }
get isEmpty: boolean { return this.items.length === 0; }
}
// Stack.test.ts
import { Stack } from './Stack';
describe('Stack', () => {
let stack: Stack<number>;
beforeEach(() => {
stack = new Stack();
});
test('пустой при создании', () => {
expect(stack.isEmpty).toBe(true);
expect(stack.size).toBe(0);
});
test('push добавляет элемент', () => {
stack.push(1);
expect(stack.size).toBe(1);
expect(stack.isEmpty).toBe(false);
});
test('pop возвращает последний элемент и удаляет его', () => {
stack.push(1);
stack.push(2);
expect(stack.pop()).toBe(2);
expect(stack.size).toBe(1);
});
test('peek возвращает элемент не удаляя', () => {
stack.push(42);
expect(stack.peek).toBe(42);
expect(stack.size).toBe(1);
});
test('pop из пустого стека возвращает undefined', () => {
expect(stack.pop()).toBeUndefined();
});
});
Тестирование класса с зависимостью
// NotificationService.ts
export class NotificationService {
constructor(private mailer: { send(to: string, text: string): Promise<void> }) {}
async notifyUser(email: string, message: string): Promise<void> {
await this.mailer.send(email, message);
}
}
// NotificationService.test.ts
import { NotificationService } from './NotificationService';
test('отправляет письмо пользователю', async () => {
const mailerMock = { send: jest.fn.mockResolvedValue(undefined) };
const service = new NotificationService(mailerMock);
await service.notifyUser('alice@test.com', 'Hello!');
expect(mailerMock.send).toHaveBeenCalledWith('alice@test.com', 'Hello!');
});
Тестирование статических методов
class MathHelper {
static clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
}
test('clamp ограничивает значение', () => {
expect(MathHelper.clamp(5, 0, 10)).toBe(5);
expect(MathHelper.clamp(-5, 0, 10)).toBe(0);
expect(MathHelper.clamp(15, 0, 10)).toBe(10);
});
Частые ошибки
- Тест приватных свойств — доступ к
instance._privateFieldнарушает инкапсуляцию; тестируй только публичный интерфейс - Создание экземпляра без
beforeEach— один экземпляр между тестами накапливает состояние - Мок всего модуля когда нужен spy —
jest.mock('./module')заменяет всё; для частичного мока используйjest.spyOn