Тестирование модулей и классов

Тестирование 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 — один экземпляр между тестами накапливает состояние
  • Мок всего модуля когда нужен spyjest.mock('./module') заменяет всё; для частичного мока используй jest.spyOn

Связанные темы

Ресурсы