Тест: структура (Arrange, Act, Assert)

AAA (Arrange, Act, Assert) — паттерн структурирования тестов на три чёткие фазы: подготовка данных, выполнение действия, проверка результата.

Зачем нужно

Единая структура делает тесты читаемыми и предсказуемыми. Новый разработчик сразу понимает: что настраивалось, что вызывалось и что проверялось. Без AAA тесты превращаются в запутанный микс setup, действий и assertions.

Где используется

  • Все виды тестов: unit, integration, E2E
  • Jest, Vitest, Playwright, Cypress
  • Эквивалент Given-When-Then в BDD-стиле

Основной контент

Базовая структура

test('описание сценария', () => {
  // Arrange — подготовка
  const price = 1000;
  const discountPercent = 20;

  // Act — действие
  const result = applyDiscount(price, discountPercent);

  // Assert — проверка
  expect(result).toBe(800);
});

Более сложный пример

test('UserService.register создаёт пользователя и отправляет email', async () => {
  // Arrange
  const emailService = { send: jest.fn.mockResolvedValue(true) };
  const userRepo = { save: jest.fn.mockResolvedValue({ id: 1, email: 'alice@test.com' }) };
  const service = new UserService({ emailService, userRepo });
  const input = { name: 'Alice', email: 'alice@test.com', password: 'Pass123!' };

  // Act
  const user = await service.register(input);

  // Assert
  expect(user.id).toBeDefined();
  expect(userRepo.save).toHaveBeenCalledWith(expect.objectContaining({ name: 'Alice' }));
  expect(emailService.send).toHaveBeenCalledWith({
    to: 'alice@test.com',
    subject: 'Добро пожаловать!',
  });
});

AAA в BDD-стиле (Given-When-Then)

// Given → Arrange
// When  → Act
// Then  → Assert

describe('Корзина покупок', () => {
  it('обновляет итог после добавления товара', () => {
    // Given
    const cart = new Cart();
    const product = { id: 1, price: 500 };

    // When
    cart.add(product);

    // Then
    expect(cart.total).toBe(500);
  });
});

AAA в E2E тестах

test('пользователь может войти', async ({ page }) => {
  // Arrange
  await page.goto('/login');

  // Act
  await page.fill('[data-testid="email"]', 'user@example.com');
  await page.fill('[data-testid="password"]', 'secret');
  await page.click('[data-testid="submit"]');

  // Assert
  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByTestId('welcome')).toBeVisible;
});

Один тест — одно утверждение

// ПЛОХО: смешано несколько аспектов
test('пользователь', () => {
  const user = createUser('Alice');
  expect(user.name).toBe('Alice');
  expect(user.createdAt).toBeDefined();
  expect(user.role).toBe('user');
  // Если один expect падает — непонятно какой именно аспект сломан
});

// ХОРОШО: отдельные тесты с ясным намерением
test('имя устанавливается при создании', () => {
  expect(createUser('Alice').name).toBe('Alice');
});
test('дата создания проставляется автоматически', () => {
  expect(createUser('Alice').createdAt).toBeDefined();
});

Частые ошибки

  • Отсутствие фаз — Act и Assert смешаны: expect(service.call(data)).toBe(...) — трудно читать и отлаживать
  • Слишком много в Arrange — если setup занимает 30+ строк, вынеси его в beforeEach или фабричную функцию
  • Несколько Act в одном тесте — проверяет несколько сценариев; раздели на отдельные тесты
  • Assert до Act — логическая ошибка; всегда сначала выполни действие, потом проверяй результат

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

Ресурсы