Написание тестов для PHPUnit
Тесты для класса Email помещаются в класс EmailTest. EmailTest наследуется от PHPUnit\Framework\TestCase. Методы тестирования — это открытые методы с именами test*.
В качестве альтернативы можно использовать атрибут PHPUnit\Framework\Attributes\Test для метода, чтобы пометить его как метод тестирования. Подробнее см. в разделе Test.
В методах тестирования используются методы утверждений, например assertSame() (см. раздел Утверждения), чтобы проверить, соответствует ли фактическое значение ожидаемому.
<?php declare(strict_types=1);
final class Email
{
private string $email;
public static function fromString(string $email): self
{
return new self($email);
}
private function __construct(string $email)
{
$this->ensureIsValidEmail($email);
$this->email = $email;
}
public function asString(): string
{
return $this->email;
}
private function ensureIsValidEmail(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException(
sprintf(
'"%s" is not a valid email address',
$email,
),
);
}
}
}
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class EmailTest extends TestCase
{
public function testCanBeCreatedFromValidEmail(): void
{
$string = 'user@example.org';
$email = Email::fromString($string);
$this->assertSame($string, $email->asString());
}
public function testCannotBeCreatedFromInvalidEmail(): void
{
$this->expectException(InvalidArgumentException::class);
Email::fromString('invalid');
}
}
Запуск приведённого выше теста даёт следующий результат:
./tools/phpunit tests/EmailTest.php
PHPUnit 13.2.0 by Sebastian Bergmann and contributors.
Runtime: PHP 8.5.7
.. 2 / 2 (100%)
Time: 00:00.001, Memory: 28.00 MB
OK (2 tests, 2 assertions)
В оставшейся части этой главы для объяснения основ написания тестов для PHPUnit используются Email и EmailTest.
Проверка возвращаемых значений
Обычно тест следует структуре «Подготовка, Действие, Проверка»:
Подготовка: настройте все необходимые предварительные условия и входные данные
Действие: выполните проверяемое действие
Проверка: убедитесь, что получены ожидаемые результаты
Рассмотрим testCanBeCreatedFromValidEmail() из приведённого выше примера с точки зрения структуры «Подготовка, Действие, Проверка»:
public function testCanBeCreatedFromValidEmail(): void
{
// Arrange
$string = 'user@example.org';
// Act
$email = Email::fromString($string);
// Assert
$this->assertSame($string, $email->asString());
}
На этапе Подготовки задаются входные данные: строка 'user@example.org'.
На этапе Действия вызывается Email::fromString(), чтобы создать объект Email из этой строки.
На этапе Проверки используется assertSame(), чтобы убедиться, что строковое представление объекта Email совпадает с исходными входными данными.
Проверка возвращаемых значений таким способом — самая распространённая операция в методе тестирования. Полный список методов утверждений, предоставляемых PHPUnit, см. в разделе Утверждения.
В идеале каждый метод тестирования должен проверять только один аспект тестируемой системы. Однако в testCanBeCreatedFromValidEmail() мы фактически проверяем два условия: объект Email можно создать из допустимой строки (именованный конструктор не вызывает исключение, и мы получаем объект Email), а asString() возвращает строковое представление объекта-значения Email. Это типичное отступление от нормы при проверке сценария успешного выполнения конструктора.
Сценарий успешного выполнения — это сценарий, в котором всё работает ожидаемым образом: передаются допустимые входные данные, и система ведёт себя правильно. Противоположный ему сценарий неуспешного выполнения (также называемый сценарием ошибки) — это сценарий, в котором что-то идёт не так, например передаются недопустимые входные данные и ожидается исключение. Проверка сценария успешного выполнения конструктора часто включает подтверждение того, что объект успешно создан и содержит ожидаемое состояние.
Ожидание исключений
Если ожидается, что действие вызовет исключение, тест следует структуре «Подготовка, Ожидание, Действие». Утверждение (ожидание) задаётся до действия, которое должно вызвать исключение.
Рассмотрим testCannotBeCreatedFromInvalidEmail() из приведённого выше примера с точки зрения структуры «Подготовка, Ожидание, Действие»:
public function testCannotBeCreatedFromInvalidEmail(): void
{
// Expect
$this->expectException(InvalidArgumentException::class);
// Act
Email::fromString('invalid');
}
На этапе Ожидания PHPUnit сообщается, что код должен вызвать исключение InvalidArgumentException.
На этапе Действия вызывается Email::fromString() с недопустимым адресом электронной почты. Если ожидаемое исключение не будет вызвано, тест завершится неудачно.
Метод expectException() необходимо вызвать до выполнения кода, который должен вызвать исключение. Именно поэтому используется структура «Подготовка, Ожидание, Действие», а не «Подготовка, Действие, Проверка».
Помимо метода expectException() существуют методы expectExceptionCode(), expectExceptionMessageIs(), expectExceptionMessageIsOrContains() и expectExceptionMessageMatches() для задания ожиданий относительно исключений, вызываемых проверяемым кодом.
Метод expectExceptionMessageIs() выполняет точное сравнение строк ожидаемого и фактического сообщений исключения.
Метод expectExceptionMessageIsOrContains() проверяет, совпадает ли фактическое сообщение исключения с ожидаемым или содержит ли оно ожидаемый текст. Если ожидаемое сообщение — пустая строка, метод проверяет, что фактическое сообщение исключения также является пустой строкой. В противном случае он проверяет, что фактическое сообщение исключения содержит ожидаемый текст.
Устарело
Метод expectExceptionMessage() объявлен устаревшим в PHPUnit 13.2 и будет удалён в PHPUnit 15. Вместо него используйте expectExceptionMessageIsOrContains().
Проверка побочных эффектов
Не всё, что нужно тестировать, является возвращаемым значением или исключением. У метода могут быть побочные эффекты: он может вызвать метод взаимодействующего объекта или выполнить операцию ввода-вывода, например записать данные в базу данных или файловую систему.
Иногда тестируемая система зависит от значений, возвращаемых взаимодействующим объектом. Заглушки тестов позволяют управлять этими косвенными входными данными.
Иногда требуется проверить, что тестируемая система вызывает метод взаимодействующего объекта с ожидаемыми аргументами. Mock-объекты позволяют проверять эти косвенные выходные данные.
Заглушки тестов и Mock-объекты подробно рассматриваются в главе о тестовых двойниках.
Поставщики данных
Метод тестирования может принимать произвольные аргументы. Эти аргументы должны предоставляться одним или несколькими методами-поставщиками данных (в приведённом ниже примере это additionProvider()). Метод-поставщик данных указывается с помощью атрибута PHPUnit\Framework\Attributes\DataProvider.
Метод-поставщик данных должен быть public и static, а его имя не должно начинаться с test. Он должен возвращать значение, которое является итерируемым: массив или объект, реализующий интерфейс Traversable. На каждом шаге итерации он должен выдавать массив. Для каждого такого массива метод тестирования будет вызван с его содержимым в качестве аргументов.
В качестве альтернативы можно использовать атрибут DataProviderClosure, чтобы определить поставщик данных непосредственно в виде статического замыкания, не создавая отдельный статический метод.
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
final class NumericDataSetsTest extends TestCase
{
public static function additionProvider(): array
{
return [
[0, 0, 0],
[0, 1, 1],
[1, 0, 1],
[1, 1, 3],
];
}
#[DataProvider('additionProvider')]
public function testAdd(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $a + $b);
}
}
Запуск приведённого выше теста даёт следующий результат:
./tools/phpunit tests/NumericDataSetsTest.php
PHPUnit 13.2.0 by Sebastian Bergmann and contributors.
Runtime: PHP 8.5.7
...F 4 / 4 (100%)
Time: 00:00.001, Memory: 28.00 MB
There was 1 failure:
1) NumericDataSetsTest::testAdd#3 with data (1, 1, 3)
Failed asserting that 2 is identical to 3.
/path/to/tests/NumericDataSetsTest.php:20
FAILURES!
Tests: 4, Assertions: 4, Failures: 1.
Полезно присваивать каждому набору данных строковый ключ. Вывод будет более подробным, поскольку будет содержать имя набора данных, из-за которого тест завершился неудачно.
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
final class NamedDataSetsTest extends TestCase
{
public static function additionProvider(): array
{
return [
'adding zeros' => [0, 0, 0],
'zero plus one' => [0, 1, 1],
'one plus zero' => [1, 0, 1],
'one plus one' => [1, 1, 3],
];
}
#[DataProvider('additionProvider')]
public function testAdd(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $a + $b);
}
}
Запуск приведённого выше теста даёт следующий результат:
./tools/phpunit tests/NamedDataSetsTest.php
PHPUnit 13.2.0 by Sebastian Bergmann and contributors.
Runtime: PHP 8.5.7
...F 4 / 4 (100%)
Time: 00:00.001, Memory: 28.00 MB
There was 1 failure:
1) NamedDataSetsTest::testAdd@one plus one with data (1, 1, 3)
Failed asserting that 2 is identical to 3.
/path/to/tests/NamedDataSetsTest.php:20
FAILURES!
Tests: 4, Assertions: 4, Failures: 1.
Примечание
Можно сделать вывод теста более подробным, определив предложение и используя имена параметров теста в качестве заполнителей (в приведённом выше примере это $a, $b и $expected) с атрибутом TestDox. Также можно указать имя именованного набора данных с помощью $_dataName.
Отдельные наборы данных можно выбрать в командной строке с помощью параметра --filter; синтаксис см. в разделе Фильтрация по имени теста.
Если тест получает входные данные как от метода-поставщика данных, так и от одного или нескольких тестов, от которых он зависит, аргументы поставщика данных будут идти перед аргументами зависимых тестов. Аргументы зависимых тестов будут одинаковыми для каждого набора данных.
Если тест зависит от теста, использующего поставщики данных, зависящий тест будет выполнен, если зависимый от него тест успешно пройдёт хотя бы для одного набора данных. Результат теста, использующего поставщики данных, нельзя передать зависящему тесту.
Все поставщики данных выполняются до вызова как метода before-class, например setUpBeforeClass(), так и первого вызова метода before-test, например setUp(). Поэтому внутри поставщика данных нельзя обращаться к свойствам фактического объекта тестового случая. Это также означает, что во время выполнения методов-поставщиков данных не собираются данные о покрытии кода.
Наборы данных, предоставляемые методом-поставщиком данных, должны содержать только скалярные значения (или массивы скалярных значений), неизменяемые объекты-значения или заглушки тестов. В методе-поставщике данных не следует создавать сервисы или большие графы объектов. Mock-объекты нельзя создавать в методе-поставщике данных.
Зависимости тестов
PHPUnit поддерживает объявление явных зависимостей между методами тестирования. Такие зависимости не определяют порядок выполнения методов тестирования, но позволяют производителю возвращать экземпляр тестового окружения и передавать его зависимым потребителям.
Производитель — это метод тестирования, возвращающий тестируемую единицу в качестве результата.
Потребитель — это метод тестирования, зависящий от одного или нескольких производителей и их возвращаемых значений.
В этом примере показано, как использовать атрибут PHPUnit\Framework\Attributes\Depends для выражения зависимостей между методами тестирования:
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\Depends;
use PHPUnit\Framework\TestCase;
final class StackTest extends TestCase
{
public function testEmpty(): array
{
$stack = [];
$this->assertEmpty($stack);
return $stack;
}
#[Depends('testEmpty')]
public function testPush(array $stack): array
{
$stack[] = 'foo';
$this->assertSame('foo', $stack[count($stack) - 1]);
$this->assertNotEmpty($stack);
return $stack;
}
#[Depends('testPush')]
public function testPop(array $stack): void
{
$this->assertSame('foo', array_pop($stack));
$this->assertEmpty($stack);
}
}
Запуск приведённого выше теста даёт следующий результат:
./tools/phpunit tests/StackTest.php
PHPUnit 13.2.0 by Sebastian Bergmann and contributors.
Runtime: PHP 8.5.7
... 3 / 3 (100%)
Time: 00:00.001, Memory: 28.00 MB
OK (3 tests, 5 assertions)
В приведённом выше примере первый тест, testEmpty(), создаёт новый массив и проверяет, что он пуст. Затем тест возвращает тестовое окружение в качестве результата. Второй тест, testPush(), зависит от testEmpty(), и результат этого зависимого теста передаётся ему в качестве аргумента. Наконец, testPop() зависит от testPush().
Примечание
По умолчанию возвращаемое производителем значение передаётся потребителям «как есть». Это означает, что если производитель возвращает объект, потребителям передаётся ссылка на этот объект. Вместо ссылки можно также передать (а) (глубокую) копию с помощью DependsUsingDeepClone или (б) обычный поверхностный клон, созданный на основе ключевого слова PHP clone, с помощью DependsUsingShallowClone.
Чтобы локализовать дефекты, нужно сосредоточить внимание на соответствующих тестах, завершившихся неудачно. Поэтому PHPUnit пропускает выполнение теста, если тест, от которого он зависит, завершился неудачно. Это упрощает локализацию дефектов за счёт использования зависимостей между тестами, как показано в примере 2.6.
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\Depends;
use PHPUnit\Framework\TestCase;
final class DependencyFailureTest extends TestCase
{
public function testOne(): void
{
$this->assertTrue(false);
}
#[Depends('testOne')]
public function testTwo(): void
{
}
}
Запуск приведённого выше теста даёт следующий результат:
./tools/phpunit --display-skipped tests/DependencyFailureTest.php
PHPUnit 13.2.0 by Sebastian Bergmann and contributors.
Runtime: PHP 8.5.7
FS 2 / 2 (100%)
Time: 00:00, Memory: 28.00 MB
There was 1 failure:
1) DependencyFailureTest::testOne
Failed asserting that false is true.
/path/to/tests/DependencyFailureTest.php:9
--
There was 1 skipped test:
1) DependencyFailureTest::testTwo
This test depends on "DependencyFailureTest::testOne" to pass
FAILURES!
Tests: 2, Assertions: 1, Failures: 1, Skipped: 1.
У теста может быть несколько атрибутов зависимостей от тестов.
По умолчанию PHPUnit не меняет порядок выполнения тестов, поэтому необходимо убедиться, что зависимости теста действительно могут быть удовлетворены до его запуска.
Если у теста есть несколько атрибутов зависимостей от тестов, он получит тестовое окружение от первого производителя в качестве первого аргумента, от второго производителя — в качестве второго аргумента и так далее.
Примечание
Можно настроить только одно ожидание для вывода. expectOutputString() и expectOutputRegex() нельзя комбинировать и нельзя вызывать более одного раза.
Незавершённые тесты
При работе над новым классом тестового случая можно начать с написания пустых методов тестирования, например:
public function testSomething(): void
{
}
чтобы отслеживать тесты, которые необходимо написать.
Проблема пустых методов тестирования в том, что они не могут завершиться неудачно и их можно ошибочно принять за успешно пройденные. Из-за этого отчёты о тестировании становятся бесполезными: невозможно понять, действительно ли тест пройден или он ещё не реализован.
Вызов $this->assertTrue(false), например, в незавершённом методе тестирования тоже не помогает, поскольку тогда тест будет считаться неудачным. Это было бы столь же ошибочно, как считать успешно пройденным нереализованный тест.
Если представить успешно пройденный тест как зелёный сигнал, а неудачный тест — как красный, нам нужен дополнительный жёлтый сигнал, чтобы пометить тест как незавершённый или ещё не реализованный.
Вызвав в методе тестирования метод markTestIncomplete(), можно пометить тест как незавершённый:
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class WorkInProgressTest extends TestCase
{
public function testSomething(): void
{
// Optional: Test anything here, if you want.
$this->assertTrue(true, 'This should already work.');
// Stop here and mark this test as incomplete.
$this->markTestIncomplete(
'This test has not been implemented yet.',
);
}
}
В выводе средства запуска тестов PHPUnit для командной строки незавершённый тест обозначается символом I, как показано в следующем примере:
./tools/phpunit --display-incomplete tests/WorkInProgressTest.php
PHPUnit 13.2.0 by Sebastian Bergmann and contributors.
Runtime: PHP 8.5.7
I 1 / 1 (100%)
Time: 00:00, Memory: 28.00 MB
There was 1 incomplete test:
1) WorkInProgressTest::testSomething
This test has not been implemented yet.
/path/to/tests/WorkInProgressTest.php:12
OK, but there were issues!
Tests: 1, Assertions: 1, Incomplete: 1.
Пропуск тестов
Не все тесты можно запускать в любой среде. Например, рассмотрим уровень абстракции базы данных с несколькими драйверами для разных поддерживаемых систем баз данных. Тесты драйвера PostgreSQL можно запускать только при наличии сервера PostgreSQL.
В примере 2.8 показан класс тестового случая DatabaseTest, содержащий один метод тестирования testConnection(). В шаблонном методе setUp() класса тестового случая проверяется наличие расширения pgsql, а метод markTestSkipped() используется для пропуска теста, если расширение отсутствует.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class DatabaseTest extends TestCase
{
protected function setUp(): void
{
if (!extension_loaded('pgsql')) {
$this->markTestSkipped(
'The PostgreSQL extension is not available',
);
}
}
public function testConnection(): void
{
// ...
}
}
В выводе средства запуска тестов PHPUnit для командной строки пропущенный тест обозначается символом S, как показано в следующем примере:
./tools/phpunit --display-skipped tests/DatabaseTest.php
PHPUnit 13.2.0 by Sebastian Bergmann and contributors.
Runtime: PHP 8.5.7
S 1 / 1 (100%)
Time: 00:00, Memory: 28.00 MB
There was 1 skipped test:
1) DatabaseTest::testConnection
The PostgreSQL extension is not available
OK, but some tests were skipped!
Tests: 1, Assertions: 0, Skipped: 1.
Пропуск тестов с помощью атрибутов
Помимо метода markTestSkipped(), для указания общих предварительных условий тестового случая можно использовать атрибуты:
RequiresPhp(string $versionRequirement)пропускает тест, если версия PHP не соответствует указаннойRequiresPhpExtension(string $extension, ?string $versionRequirement)пропускает тест, если указанное расширение PHP недоступноRequiresSetting(string $setting, string $value)пропускает тест, если указанная настройка конфигурации PHP не имеет заданного значенияRequiresOperatingSystem(string $regularExpression)пропускает тест, если имя операционной системы не соответствует указанному регулярному выражениюRequiresOperatingSystemFamily(string $operatingSystemFamily)пропускает тест, если семейство операционной системы не соответствует указанномуRequiresMethod(string $className, string $functionName)пропускает тест, если метод с указанным именем не объявленRequiresFunction(string $functionName)пропускает тест, если функция с указанным именем не объявленаRequiresPhpunit(string $versionRequirement)пропускает тест, если версия PHPUnit не соответствует указанной
Все перечисленные выше атрибуты объявлены в пространстве имён PHPUnit\Framework\Attributes.
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\TestCase;
#[RequiresPhpExtension('pgsql')]
final class DatabaseTest extends TestCase
{
public function testConnection(): void
{
// ...
}
}
Пользовательская логика пропуска
Перечисленные выше атрибуты Requires* предназначены для самых распространённых случаев. Они заменяют пользовательскую логику пропуска, которую иначе пришлось бы писать в методах before-class или before-test.
Если требуется логика пропуска, выходящая за рамки возможностей атрибутов Requires* (например, пропуск теста, когда расширение доступно, или при выполнении нескольких условий), используйте markTestSkipped() в методе setUp() или setUpBeforeClass() (либо в методе, настроенном с помощью атрибута #[Before] или #[BeforeClass]).
Проверка вывода
Иногда требуется проверить, что выполнение метода, например, создаёт ожидаемый вывод (например, с помощью echo или print). Для этого PHPUnit использует функцию PHP буферизации вывода.
В примере 2.10 показано, как использовать метод expectOutputString() для задания ожидаемого вывода. Если этот ожидаемый вывод не будет получен, тест будет считаться неудачным.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class OutputTest extends TestCase
{
public function testExpectFooActualFoo(): void
{
$this->expectOutputString('foo');
print 'foo';
}
public function testExpectBarActualBaz(): void
{
$this->expectOutputString('bar');
print 'baz';
}
}
Запуск приведённого выше теста даёт следующий результат:
./tools/phpunit tests/OutputTest.php
PHPUnit 13.2.0 by Sebastian Bergmann and contributors.
Runtime: PHP 8.5.7
.F 2 / 2 (100%)
Time: 00:00.001, Memory: 28.00 MB
There was 1 failure:
1) OutputTest::testExpectBarActualBaz
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
Если необходимо сравнить ожидаемый вывод с регулярным выражением, вместо expectOutputString() можно использовать метод expectOutputRegex():
$this->expectOutputRegex('/foo .+ bar/');
Если сгенерированный вывод не соответствует регулярному выражению, тест будет считаться неудачным.
Проверка вывода журнала ошибок
Иногда требуется проверить, что тестируемый код вызывает функцию PHP error_log(). Во время выполнения теста PHPUnit перехватывает вывод error_log().
С помощью метода expectErrorLog() можно указать, что во время теста error_log() должна быть вызвана хотя бы один раз. Если error_log() не будет вызвана, тест будет считаться неудачным.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ErrorLogTest extends TestCase
{
public function testSomethingIsLogged(): void
{
// Code under test that calls error_log()
error_log('something happened');
$this->expectErrorLog();
}
}
Если expectErrorLog() не используется и тестируемый код вызывает error_log(), записанный вывод отображается как часть вывода теста (без префиксов с датой).
Примечание
Метод expectErrorLog() необходимо вызвать во время выполнения метода тестирования, но не имеет значения, будет ли он вызван до или после кода, вызывающего error_log(). Это согласуется с работой expectOutputString().
© 2005–2026 Sebastian Bergmann
Licensed under the Creative Commons Attribution 3.0 Unported License.
https://docs.phpunit.de/en/13.2/writing-tests-for-phpunit.html