Какие знаешь базовые действия для тестирования юнит-тестом?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Базовые действия для написания юнит-тестов
Юнит-тестирование — это фундаментальная практика разработки ПО, где проверяется минимальная возможная единица кода (обычно функция или метод) в изоляции от зависимостей. Вот ключевые действия и принципы, которые я применяю при написании юнит-тестов на 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 — это каркас для любого юнит-теста. Его эффективность достигается за счет грамотной изоляции зависимостей через моки и стабы, а также за счет покрытия широкого спектра сценариев — от позитивных до исключительных. Качественные юнит-тесты не только ловят регрессии, но и служат отличной документацией к коду, наглядно показывая, как должна использоваться та или иная функция.