Написание тестов для PHPUnit
Проверка возвращаемых значений
В этом первом примере представлены основные принципы и шаги для написания тестов с помощью PHPUnit:
Тесты для класса
Greeterпомещаются в классGreeterTest.GreeterTestнаследуется от классаPHPUnit\Framework\TestCase.-
Тесты представляют собой публичные методы, которые имеют имена
test*.В качестве альтернативы, вы можете использовать атрибут
PHPUnit\Framework\Attributes\Testдля метода, чтобы пометить его как тестовый метод. Подробнее см. раздел об атрибуте Тест. Внутри тестовых методов используются методы проверки, такие как
assertSame()(см. Утверждения), чтобы проверить, соответствует ли фактическое значение ожидаемому значению.
<?php declare(strict_types=1);
final class Greeter
{
public function greet(string $name): string
{
return 'Hello, ' . $name . '!';
}
}
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class GreeterTest extends TestCase
{
public function testGreetsWithName(): void
{
$greeter = new Greeter;
$greeting = $greeter->greet('Alice');
$this->assertSame('Hello, Alice!', $greeting);
}
}
Запуск показанного выше теста даёт следующий вывод:
./tools/phpunit tests/GreeterTest.php
PHPUnit 12.0.2 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.3
. 1 / 1 (100%)
Time: 00:00, Memory: 25.29 MB
OK (1 test, 1 assertion)
Мартин Фаулер однажды сказал:
Всякий раз, когда вы хотите ввести что-либо в утверждение
Ожидание исключений
Пример 2.3 демонстрирует использование метода expectException() для проверки того, что код, подлежащий тестированию, вызывает исключение.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ExceptionTest extends TestCase
{
public function testException(): void
{
$this->expectException(InvalidArgumentException::class);
}
}
Запуск показанного выше теста даёт следующий вывод:
./tools/phpunit tests/ExceptionTest.php
PHPUnit 12.0.2 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.3
F 1 / 1 (100%)
Time: 00:00, Memory: 25.29 MB
There was 1 failure:
1) ExceptionTest::testException
Failed asserting that exception of type "InvalidArgumentException" is thrown.
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
Метод expectException() необходимо использовать перед тем, как ожидаемое исключение будет вызвано. В идеале, expectException() вызывается непосредственно перед вызовом кода, который должен вызвать исключение.
Помимо метода expectException() существуют методы expectExceptionCode(), expectExceptionMessage() и expectExceptionMessageMatches() для настройки ожиданий относительно исключений, которые может генерировать код, подлежащий тестированию.
Примечание
Обратите внимание, что expectExceptionMessage() проверяет, содержит ли сообщение об исключении $actual сообщение $expected, а не производит точное сравнение строк.
Проверка возвращаемых значений и ожидание исключений — две из трёх наиболее часто выполняемых операций в тестовом методе. Третья — проверка побочных эффектов. Проверка побочных эффектов при взаимодействии объектов обсуждается в главе о Двойниках тестов.
Поставщики данных
Тестовый метод может принимать произвольное количество аргументов. Эти аргументы должны быть предоставлены одним или несколькими методами поставщиков данных (additionProvider() в примере ниже). Метод поставщика данных, который будет использоваться, указывается с помощью атрибута PHPUnit\Framework\Attributes\DataProvider или PHPUnit\Framework\Attributes\DataProviderExternal.
Метод поставщика данных должен быть public и static. Он должен возвращать значение, которое является итерируемым, либо массивом, либо объектом, реализующим интерфейс Traversable. На каждом шаге итерации он должен возвращать массив. Для каждого из этих массивов тестовый метод будет вызываться с содержанием массива в качестве аргументов.
<?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);
}
}
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\DataProviderExternal;
use PHPUnit\Framework\TestCase;
final class NumericDataSetsTestUsingExternalDataProvider extends TestCase
{
#[DataProviderExternal(ExternalDataProvider::class, 'additionProvider')]
public function testAdd(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $a + $b);
}
}
final class ExternalDataProvider
{
public static function additionProvider(): array
{
return [
[0, 0, 0],
[0, 1, 1],
[1, 0, 1],
[1, 1, 3],
];
}
}
Запуск показанного выше теста даёт следующий вывод:
./tools/phpunit tests/NumericDataSetsTest.php
PHPUnit 12.0.2 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.3
...F 4 / 4 (100%)
Time: 00:00.001, Memory: 25.29 MB
There was 1 failure:
1) NumericDataSetsTest::testAdd with data set #3 (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 12.0.2 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.3
...F 4 / 4 (100%)
Time: 00:00.001, Memory: 25.29 MB
There was 1 failure:
1) NamedDataSetsTest::testAdd with data set "one plus one" (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 или --exclude-group, например, выполняются перед вызовом статического метода setUpBeforeClass() и первым вызовом метода setUp(). Вы не можете получить доступ к свойствам объекта реального тестового случая в методах, таких как setUpBeforeClass() или setUp(), внутри поставщика данных.
Примечание
Во время выполнения методов поставщиков данных данные о покрытии кода не собираются.
Тестовый вывод
Иногда вы хотите утверждать, что выполнение метода, например, генерирует ожидаемый вывод (например, через echo или print). Класс PHPUnit\Framework\TestCase использует функцию буферизации вывода PHP, чтобы предоставить функциональность, необходимую для этого.
Пример 2.7 показывает, как использовать метод 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 12.0.2 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.3
.F 2 / 2 (100%)
Time: 00:00, Memory: 25.29 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.
Таблица 2.1 показывает методы, предоставляемые для тестирования вывода.
Метод | Значение |
|---|---|
| Установите ожидание, что вывод соответствует |
| Установите ожидание, что вывод равен |
Незавершенные тесты
Когда вы работаете над новым классом тестовых случаев, вы можете начать с написания пустых тестовых методов, таких как:
public function testSomething(): void
{
}
чтобы отслеживать тесты, которые вам нужно написать.
Примечание
Сделайте себе одолжение и никогда не используйте бессмысленные имена, такие как testSomething, для ваших тестовых методов.
Проблема с пустыми тестовыми методами заключается в том, что они не могут завершиться сбоем и могут быть неправильно истолкованы как успешные. Это неправильное толкование приводит к тому, что отчеты о тестах бесполезны — вы не можете увидеть, действительно ли тест успешен, или просто еще не реализован.
Вызов $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.',
);
}
}
Неполный тест обозначается I в выводе тестового исполнителя PHPUnit на командной строке, как показано в следующем примере:
./tools/phpunit --display-incomplete tests/WorkInProgressTest.php
PHPUnit 12.0.2 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.3
I 1 / 1 (100%)
Time: 00:00, Memory: 25.29 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.
Пропуск тестов
Не все тесты могут выполняться во всех средах. Рассмотрим, например, уровень абстракции базы данных, который имеет несколько драйверов для различных систем баз данных, которые он поддерживает. Тесты для драйвера MySQL могут выполняться только при наличии сервера MySQL.
Пример 2.9 показывает класс тестовых случаев, DatabaseTest, содержащий один тестовый метод, testConnection(). В методе шаблона setUp() класса тестовых случаев мы проверяем, доступен ли расширение MySQLi, и используем метод 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
{
// ...
}
}
Пропущенный тест обозначается S в выводе тестового исполнителя PHPUnit на командной строке, как показано в следующем примере:
./tools/phpunit --display-skipped tests/DatabaseTest.php
PHPUnit 12.0.2 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.3
S 1 / 1 (100%)
Time: 00:00, Memory: 25.29 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.
Пропуск тестов с помощью атрибутов
Помимо вышеперечисленных методов, также можно использовать атрибуты для выражения общих предпосылок для тестового случая:
RequiresFunction(string $functionName)пропускает тест, если функция с указанным именем не объявленаRequiresMethod(string $className, string $functionName)пропускает тест, если метод с указанным именем не объявленRequiresOperatingSystem(string $regularExpression)пропускает тест, если имя операционной системы не соответствует указанному регулярному выражениюRequiresOperatingSystemFamily(string $operatingSystemFamily)пропускает тест, если семейство операционной системы не указаноRequiresPhp(string $versionRequirement)пропускает тест, если версия PHP не соответствует указаннойRequiresPhpExtension(string $extension, ?string $versionRequirement)пропускает тест, если указанное расширение PHP недоступноRequiresPhpunit(string $versionRequirement)пропускает тест, если версия PHPUnit не соответствует указаннойRequiresSetting(string $setting, string $value)пропускает тест, если указанное значение настройки PHP не установлено в указанное значение
Все перечисленные выше атрибуты объявлены в пространстве имен 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
{
// ...
}
}
Зависимости тестов
Адриан Кун и др. написали:
Единые тесты в первую очередь пишутся как хорошая практика для помощи разработчикам в выявлении и исправлении ошибок, для рефакторинга кода и для использования в качестве документации для тестируемой единицы программного обеспечения. Для достижения этих преимуществ единые тесты должны в идеале охватывать все возможные пути в программе. Один единый тест обычно охватывает один конкретный путь в одной функции или методе. Однако тестовый метод не обязательно является инкапсулированной, независимой сущностью. Часто между тестовыми методами существуют неявные зависимости, скрытые в сценарии реализации теста.
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 12.0.2 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.3
... 3 / 3 (100%)
Time: 00:00, Memory: 25.29 MB
OK (3 tests, 5 assertions)
В примере выше первый тест, testEmpty(), создает новый массив и утверждает, что он пуст. Затем тест возвращает фикстуру в качестве результата. Второй тест, testPush(), зависит от testEmpty() и получает результат этого зависимого теста в качестве аргумента. Наконец, testPop() зависит от testPush().
Примечание
Значение возврата, возвращаемое производителем, по умолчанию передается «как есть» его потребителям. Это означает, что при возврате объектом производителем ссылка на этот объект передается потребителям. Вместо ссылки либо (а) (глубокая) копия через DependsUsingDeepClone, либо (б) (обычная поверхностная) копия (на основе PHP-ключевого слова clone) через DependsUsingShallowClone также возможны.
Для локализации дефектов мы хотим, чтобы наше внимание было сосредоточено на соответствующих ошибочных тестах. Именно поэтому PHPUnit пропускает выполнение теста, когда зависимый тест завершился неудачно. Это улучшает локализацию дефектов за счет использования зависимостей между тестами, как показано в примере 2.12.
<?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 12.0.2 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.3
FS 2 / 2 (100%)
Time: 00:00, Memory: 25.29 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 не изменяет порядок выполнения тестов, поэтому необходимо убедиться, что зависимости теста могут быть выполнены до запуска теста.
Тест с более чем одним атрибутом зависимости теста получит фикстуру от первого производителя в качестве первого аргумента, фикстуру от второго производителя в качестве второго аргумента и так далее.
Вывод об ошибках
В случае неудачи теста PHPUnit пытается предоставить вам как можно больше контекста, который может помочь в выявлении проблемы.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ArrayDiffTest extends TestCase
{
public function testEquality(): void
{
$this->assertSame(
[1, 2, 3, 4, 5, 6],
[1, 2, 33, 4, 5, 6],
);
}
}
Запуск показанного выше теста приводит к выводу, показанному ниже:
./tools/phpunit tests/ArrayDiffTest.php
PHPUnit 12.0.2 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.3
F 1 / 1 (100%)
Time: 00:00, Memory: 25.29 MB
There was 1 failure:
1) ArrayDiffTest::testEquality
Failed asserting that two arrays are identical.
--- Expected
+++ Actual
@@ @@
Array &0 [
0 => 1,
1 => 2,
- 2 => 3,
+ 2 => 33,
3 => 4,
4 => 5,
5 => 6,
]
/path/to/tests/ArrayDiffTest.php:8
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
В этом примере отличается только одно из значений массива, а другие значения отображаются, чтобы предоставить контекст о том, где произошла ошибка.
Когда сгенерированный вывод будет длинным для чтения, PHPUnit разделит его и предоставит несколько строк контекста вокруг каждого различия.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class LongArrayDiffTest extends TestCase
{
public function testEquality(): void
{
$this->assertSame(
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 33, 4, 5, 6],
);
}
}
Запуск показанного выше теста приводит к выводу, показанному ниже:
./tools/phpunit tests/LongArrayDiffTest.php
PHPUnit 12.0.2 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.3
F 1 / 1 (100%)
Time: 00:00.001, Memory: 25.29 MB
There was 1 failure:
1) LongArrayDiffTest::testEquality
Failed asserting that two arrays are identical.
--- Expected
+++ Actual
@@ @@
11 => 0,
12 => 1,
13 => 2,
- 14 => 3,
+ 14 => 33,
15 => 4,
16 => 5,
17 => 6,
]
/path/to/tests/LongArrayDiffTest.php:8
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
Крайние случаи
При неудачном сравнении PHPUnit создает текстовые представления входных значений и сравнивает их. Из-за этой реализации различие может показывать больше проблем, чем на самом деле существует.
Это происходит только при использовании assertEquals() или других «слабых» функций сравнения для массивов или объектов.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ArrayWeakComparisonTest extends TestCase
{
public function testEquality(): void
{
$this->assertEquals(
[1, 2, 3, 4, 5, 6],
['1', 2, 33, 4, 5, 6],
);
}
}
Запуск показанного выше теста приводит к выводу, показанному ниже:
./tools/phpunit tests/ArrayWeakComparisonTest.php
PHPUnit 12.0.2 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.3
F 1 / 1 (100%)
Time: 00:00, Memory: 25.29 MB
There was 1 failure:
1) ArrayWeakComparisonTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
Array (
- 0 => 1
+ 0 => '1'
1 => 2
- 2 => 3
+ 2 => 33
3 => 4
4 => 5
5 => 6
)
/path/to/tests/ArrayWeakComparisonTest.php:8
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
В этом примере различие в первом индексе между 1 и '1' сообщается, даже если assertEquals() рассматривает значения как совпадающие.
© 2005–2025 Sebastian Bergmann
Licensed under the Creative Commons Attribution 3.0 Unported License.
https://docs.phpunit.de/en/12.0/writing-tests-for-phpunit.html