← Назад к вопросам

Какие знаешь базовые действия для тестирования юнит-тестом?

1.6 Junior🔥 162 комментариев
#Тестирование

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Базовые действия для написания юнит-тестов

Юнит-тестирование — это фундаментальная практика разработки ПО, где проверяется минимальная возможная единица кода (обычно функция или метод) в изоляции от зависимостей. Вот ключевые действия и принципы, которые я применяю при написании юнит-тестов на PHP (с использованием PHPUnit).

1. Подготовка изолированного окружения (Arrange)

Перед выполнением теста необходимо подготовить все необходимые данные и объекты. Это включает:

  • Создание экземпляра тестируемого класса (SUT — System Under Test).
  • Настройку фиктивных зависимостей (Mock Objects) с помощью библиотек вроде mockery/mockery или встроенных возможностей PHPUnit. Это позволяет изолировать тестируемый метод от базы данных, внешних API, файловой системы и других классов.
  • Определение входных данных и ожидаемого результата.
// Пример фазы Arrange
public function testCanTransferMoneyBetweenAccounts(): void
{
    // 1. Создаем фиктивный объект (зависимость)
    $repositoryMock = $this->createMock(AccountRepositoryInterface::class);

    // 2. Настраиваем фиктивный объект на возвращение конкретных данных
    $repositoryMock->method('findById')
        ->willReturn(new Account(1, 1000));

    // 3. Создаем экземпляр тестируемого сервиса, инжектим фиктивную зависимость
    $transferService = new MoneyTransferService($repositoryMock);

    // 4. Определяем входные данные
    $fromAccountId = 1;
    $toAccountId = 2;
    $amount = 100;
}

2. Выполнение тестируемого действия (Act)

На этом этапе вызывается непосредственно тот метод, который мы тестируем, с подготовленными входными данными. Действие должно быть одним.

// Продолжение примера: фаза Act
    // Выполняем единственное тестируемое действие
    $result = $transferService->transfer($fromAccountId, $toAccountId, $amount);

3. Проверка результата (Assert)

После выполнения действия необходимо убедиться, что:

  • Возвращаемое значение соответствует ожиданиям.
  • Состояние тестируемого объекта изменилось предсказуемым образом.
  • Были вызваны ожидаемые методы на зависимостях (mock-объектах) с правильными аргументами.
// Продолжение примера: фаза Assert
    // Проверяем возвращаемое значение
    $this->assertTrue($result);

    // Проверяем, что метод save был вызван на mock-объекте с ожидаемыми аргументами
    $repositoryMock->expects($this->exactly(2))
        ->method('save')
        ->withConsecutive(
            [$this->callback(fn($acc) => $acc->getId() === 1 && $acc->getBalance() === 900)],
            [$this->callback(fn($acc) => $acc->getId() === 2 && $acc->getBalance() === 1100)]
        );

4. Ключевые паттерны и типы проверок

В рамках этих трех фаз я постоянно использую несколько важных паттернов:

  • Тестирование публичного контракта: Проверяю только публичные методы и их observable поведение, не приватные детали реализации.
  • Разные сценарии для одного метода:
    *   **Happy Path (позитивный сценарий):** Корректные данные, успешное выполнение.
    *   **Edge Cases (граничные условия):** Пустые строки, нулевые значения, пределы диапазонов (например, `PHP_INT_MAX`).
    *   **Negative Tests (негативные сценарии):** Проверка исключительных ситуаций и выбрасываемых исключений.

// Пример тестирования исключения (негативный сценарий)
public function testTransferFailsWhenInsufficientFunds(): void
{
    $this->expectException(InsufficientFundsException::class);
    $this->expectExceptionMessage('Not enough money on account');

    $repositoryMock = $this->createMock(AccountRepositoryInterface::class);
    $repositoryMock->method('findById')->willReturn(new Account(1, 50));

    $service = new MoneyTransferService($repositoryMock);
    $service->transfer(1, 2, 100); // Попытка перевести больше, чем есть
}
  • Использование Data Providers: Для тестирования одного метода с множеством наборов входных данных и ожидаемых результатов, что делает тесты более чистыми и удобными в поддержке.
// Пример использования Data Provider
/**
 * @dataProvider additionProvider
 */
public function testAdd(int $a, int $b, int $expected): void
{
    $this->assertSame($expected, Calculator::add($a, $b));
}

public function additionProvider(): array
{
    return [
        'Positive numbers' => [2, 3, 5],
        'Zero addition'    => [0, 5, 5],
        'Negative numbers' => [-1, -1, -2],
    ];
}

5. Дополнительные важные аспекты

  • Именование тестов: Имя тестового метода должно четко описывать сценарий (testTransferFailsWhenInsufficientFunds лучше, чем testTransfer1).
  • Принцип FIRST: Хорошие юнит-тесты должны быть Fast (быстрыми), Isolated (изолированными), Repeatable (повторяемыми), Self-validating (самовалидирующимися) и Timely (своевременными).
  • Тестирование без побочных эффектов: Каждый тест должен запускаться в чистом окружении и не должен влиять на результат выполнения других тестов. Для этого часто используют setUp() и tearDown() методы в PHPUnit.

Итог: Базовый цикл Arrange-Act-Assert — это каркас для любого юнит-теста. Его эффективность достигается за счет грамотной изоляции зависимостей через моки и стабы, а также за счет покрытия широкого спектра сценариев — от позитивных до исключительных. Качественные юнит-тесты не только ловят регрессии, но и служат отличной документацией к коду, наглядно показывая, как должна использоваться та или иная функция.