Небольшие правила, большая разница
Хорошие тесты следуют нескольким привычкам:
1. Понятные имена — testGreetEmpty говорит, что сломалось, без открытия файла.
2. Одно поведение на тест — небольшие тесты указывают прямо на проблему.
3. Провайдеры данных для множества случаев — одна таблица вместо скопированных тестов.
4. Проверки, которые объясняют себя сами — assertSame при провале показывает ожидаемое и фактическое значение.
5. Отдельные методы, чтобы видеть всё; проверки подряд, чтобы падать сразу.
6. Запускайте тесты часто — после каждого небольшого изменения, а не один раз в конце.
7. Тесты живут рядом с кодом — тот же проект, суффикс Test.php.
Сообщения о провале, которые помогают
Провалившийся тест читают в момент замешательства — сообщение должно рассказать всю историю: какой вызов, что вернулось, что ожидалось.
assertSame сам печатает ожидаемое и фактическое значение в виде diff; необязательный аргумент с сообщением добавляет недостающую часть — какой вызов дал это значение.
Сообщение со всей историей
greet возвращает неверное приветствие. В выводе о провале видно своё сообщение с названием вызова, а затем diff: ожидалось 'Hello, Ana!', фактически 'Hi Ana' — ошибка (не то слово, нет запятой и восклицательного знака) видна по одному только выводу, без открытия кода.
Вывод: PHP-скрипт
<?php
use PHPUnit\Framework\TestCase;
// В greet есть ошибка: не то слово, нет запятой и «!».
function greet(string $name): string {
return "Hi " . $name;
}
final class GreetTest extends TestCase {
public function testGreet(): void {
$this->assertSame("Hello, Ana!", greet("Ana"), 'вызов greet("Ana") вернул неверное приветствие');
}
}
Одно поведение на тест
У теста, который проверяет пять вещей, есть пять причин провалиться — а его имя может описать лишь одну из них. Разделение по поведениям делает каждый тест небольшим, а его имя — честным.
Здесь обычное приветствие и подстановка для пустого имени — это два поведения, поэтому им достаются два тестовых метода. Когда один из них провалится, его имя уже скажет, какое поведение сломалось.
Два поведения, два теста
testGreetName проверяет обычный случай, testGreetEmpty — подстановку. Вывод --testdox перечисляет их отдельно — провал указал бы ровно на одно поведение.
Вывод: PHP-скрипт
<?php
use PHPUnit\Framework\TestCase;
function greet(string $name): string {
if ($name === "") {
return "Hello, stranger!";
}
return "Hello, " . $name . "!";
}
final class GreetTest extends TestCase {
public function testGreetName(): void {
$this->assertSame("Hello, Ana!", greet("Ana"));
}
public function testGreetEmpty(): void {
$this->assertSame("Hello, stranger!", greet(""));
}
}
Сколько протестировано: покрытие
PHPUnit умеет измерять, какие строки вашего кода тесты действительно выполняют. Для этого в PHP нужно расширение для покрытия — PCOV или Xdebug — и найденное расширение он сообщает в строке заголовка.
./vendor/bin/phpunit --coverage-filter . --coverage-text DescribeTest.php измеряет файлы в текущей папке и печатает отчёт после результата тестов.
Число — это компас, а не цель: низкое значение показывает непроверенные пути — здесь тест проходит только по ветке mild, поэтому строки freezing и hot никогда не выполняются. Покрытие 100% не доказывает, что код верен, но непокрытые строки гарантированно не проверены.
Измерить покрытие
У describeTemperature три ветки, тест задействует одну. Отчёт считает каждую исполняемую строку файла — включая строку самого тестового метода — и сообщает Lines: 66.66% (4/6): две непокрытые строки — это возвраты freezing и hot. Добавление тестов для холодной и жаркой ветки подняло бы это число.
Вывод: PHP-скрипт
<?php
use PHPUnit\Framework\TestCase;
function describeTemperature(int $celsius): string {
if ($celsius < 0) {
return "freezing";
}
if ($celsius > 30) {
return "hot";
}
return "mild";
}
final class DescribeTest extends TestCase {
public function testDescribeMild(): void {
$this->assertSame("mild", describeTemperature(20));
}
}