Написание тестов для PHPUnit
Пример 2.1 демонстрирует, как писать тесты с помощью PHPUnit для проверки операций с массивами в PHP. Пример представляет основные соглашения и шаги для написания тестов с PHPUnit:
-
Тесты для класса
Classнаходятся в классеClassTest. -
ClassTest(в большинстве случаев) наследуется отPHPUnit\Framework\TestCase. -
Тесты — это публичные методы, которые имеют имена
test*.В качестве альтернативы можно использовать аннотацию
@testв блоке документации метода, чтобы отметить его как метод теста. -
Внутри методов тестов используются методы утверждения, такие как
assertSame()(см. Утверждения), чтобы утверждать, что фактическое значение соответствует ожидаемому.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StackTest extends TestCase
{
public function testPushAndPop(): void
{
$stack = [];
$this->assertSame(0, count($stack));
array_push($stack, 'foo');
$this->assertSame('foo', $stack[count($stack)-1]);
$this->assertSame(1, count($stack));
$this->assertSame('foo', array_pop($stack));
$this->assertSame(0, count($stack));
}
}
Всякий раз, когда вы хотите ввести что-либо в оператор
Зависимости тестов
Адриан Кун и др.:
Тесты юнит-уровня в первую очередь предназначены для помощи разработчикам в выявлении и исправлении ошибок, рефакторинге кода и служат документацией для единицы программного обеспечения, подвергающейся тестированию. Для достижения этих преимуществ тесты юнит-уровня должны, по идее, охватывать все возможные пути в программе. Обычно один тест юнит-уровня покрывает один конкретный путь в одной функции или методе. Однако метод теста не обязательно является обособленной, независимой сущностью. Часто между методами тестов существуют неявные зависимости, скрытые в сценарии реализации теста.
PHPUnit поддерживает объявление явных зависимостей между методами тестов. Такие зависимости не определяют порядок выполнения методов тестов, но они позволяют возвращать экземпляр фикстуры теста производителем и передавать его зависимым потребителям.
- Производитель — это метод теста, который возвращает свою единицу тестирования как возвращаемое значение.
- Потребитель — это метод теста, который зависит от одного или нескольких производителей и их возвращаемых значений.
Пример 2.2 демонстрирует использование аннотации @depends для выражения зависимостей между методами тестов.
@depends для выражения зависимостей
<?php declare(strict_types=1);
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
{
array_push($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);
}
}
В примере выше первый тест, testEmpty(), создает новый массив и утверждает, что он пустой. Затем тест возвращает фикстуру в качестве результата. Второй тест, testPush(), зависит от testEmpty() и получает результат этого зависящего теста в качестве аргумента. Наконец, testPop() зависит от testPush().
Примечание
Возвращаемое значение, сгенерированное производителем, по умолчанию передаётся потребителям «как есть». Это означает, что когда производитель возвращает объект, ссылка на этот объект передается потребителям. Вместо ссылки можно также (а) сделать (глубокую) копию с помощью @depends clone, или (б) создать (обычную неглубокую) копию с помощью ключевого слова PHP clone через @depends shallowClone.
Для локализации дефектов мы хотим сфокусироваться на соответствующих ошибочных тестах. Вот почему PHPUnit пропускает выполнение теста, когда зависящий тест завершился ошибкой. Это улучшает локализация дефектов, используя зависимости между тестами, как показано в Примере 2.3.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class DependencyFailureTest extends TestCase
{
public function testOne(): void
{
$this->assertTrue(false);
}
/**
* @depends testOne
*/
public function testTwo(): void
{
}
}
$ phpunit --verbose DependencyFailureTest PHPUnit 9.5.0 by Sebastian Bergmann and contributors. FS Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) DependencyFailureTest::testOne Failed asserting that false is true. /home/sb/DependencyFailureTest.php:6 There was 1 skipped test: 1) DependencyFailureTest::testTwo This test depends on "DependencyFailureTest::testOne" to pass. FAILURES! Tests: 1, Assertions: 1, Failures: 1, Skipped: 1.
Один тест может иметь более одной аннотации @depends. PHPUnit не изменяет порядок выполнения тестов, вам нужно убедиться, что зависимости теста могут быть выполнены до запуска теста.
Тест, имеющий более одной аннотации @depends, получит фикстуру от первого производителя в качестве первого аргумента, фикстуру от второго производителя в качестве второго аргумента и так далее. См. Пример 2.4
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class MultipleDependenciesTest extends TestCase
{
public function testProducerFirst(): string
{
$this->assertTrue(true);
return 'first';
}
public function testProducerSecond(): string
{
$this->assertTrue(true);
return 'second';
}
/**
* @depends testProducerFirst
* @depends testProducerSecond
*/
public function testConsumer(string $a, string $b): void
{
$this->assertSame('first', $a);
$this->assertSame('second', $b);
}
}
$ phpunit --verbose MultipleDependenciesTest PHPUnit 9.5.0 by Sebastian Bergmann and contributors. ... Time: 0 seconds, Memory: 3.25Mb OK (3 tests, 4 assertions)
Поставщики данных
Метод теста может принимать произвольные аргументы. Эти аргументы должны предоставляться одним или несколькими методами поставщиков данных (additionProvider() в Примере 2.5). Метод поставщика данных указывается с помощью аннотации @dataProvider.
Метод поставщика данных должен быть public и либо возвращать массив массивов, либо объект, реализующий интерфейс Iterator, и возвращать массив на каждом шаге итерации. Для каждого массива, который является частью набора, метод теста вызывается с содержимым массива в качестве аргументов.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class DataTest extends TestCase
{
/**
* @dataProvider additionProvider
*/
public function testAdd(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $a + $b);
}
public function additionProvider(): array
{
return [
[0, 0, 0],
[0, 1, 1],
[1, 0, 1],
[1, 1, 3]
];
}
}
$ phpunit DataTest PHPUnit 9.5.0 by Sebastian Bergmann and contributors. ...F Time: 0 seconds, Memory: 5.75Mb There was 1 failure: 1) DataTest::testAdd with data set #3 (1, 1, 3) Failed asserting that 2 is identical to 3. /home/sb/DataTest.php:9 FAILURES! Tests: 4, Assertions: 4, Failures: 1.
При использовании большого количества наборов данных полезно давать каждому имя с помощью строкового ключа вместо стандартного числового. Вывод будет более подробным, так как он будет содержать название набора данных, который приводит к ошибке теста.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class DataTest extends TestCase
{
/**
* @dataProvider additionProvider
*/
public function testAdd(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $a + $b);
}
public 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]
];
}
}
$ phpunit DataTest PHPUnit 9.5.0 by Sebastian Bergmann and contributors. ...F Time: 0 seconds, Memory: 5.75Mb There was 1 failure: 1) DataTest::testAdd with data set "one plus one" (1, 1, 3) Failed asserting that 2 is identical to 3. /home/sb/DataTest.php:9 FAILURES! Tests: 4, Assertions: 4, Failures: 1.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class DataTest extends TestCase
{
/**
* @dataProvider additionProvider
*/
public function testAdd(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $a + $b);
}
public function additionProvider(): CsvFileIterator
{
return new CsvFileIterator('data.csv');
}
}
$ phpunit DataTest
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
...F
Time: 0 seconds, Memory: 5.75Mb
There was 1 failure:
1) DataTest::testAdd with data set #3 ('1', '1', '3')
Failed asserting that 2 is identical to 3.
/home/sb/DataTest.php:11
FAILURES!
Tests: 4, Assertions: 4, Failures: 1.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class CsvFileIterator implements Iterator
{
private $file;
private $key = 0;
private $current;
public function __construct(string $file)
{
$this->file = fopen($file, 'r');
}
public function __destruct()
{
fclose($this->file);
}
public function rewind(): void
{
rewind($this->file);
$this->current = fgetcsv($this->file);
$this->key = 0;
}
public function valid(): bool
{
return !feof($this->file);
}
public function key(): int
{
return $this->key;
}
public function current(): array
{
return $this->current;
}
public function next(): void
{
$this->current = fgetcsv($this->file);
$this->key++;
}
}
Когда тест получает входные данные как от метода @dataProvider, так и от одного или нескольких зависящих от него тестов, аргументы от поставщика данных приходят перед аргументами от зависящих тестов. Аргументы от зависящих тестов будут одинаковыми для каждого набора данных. См. Пример 2.9
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class DependencyAndDataProviderComboTest extends TestCase
{
public function provider(): array
{
return [['provider1'], ['provider2']];
}
public function testProducerFirst(): void
{
$this->assertTrue(true);
return 'first';
}
public function testProducerSecond(): void
{
$this->assertTrue(true);
return 'second';
}
/**
* @depends testProducerFirst
* @depends testProducerSecond
* @dataProvider provider
*/
public function testConsumer(): void
{
$this->assertSame(
['provider1', 'first', 'second'],
func_get_args()
);
}
}
$ phpunit --verbose DependencyAndDataProviderComboTest
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
...F
Time: 0 seconds, Memory: 3.50Mb
There was 1 failure:
1) DependencyAndDataProviderComboTest::testConsumer with data set #1 ('provider2')
Failed asserting that two arrays are identical.
--- Expected
+++ Actual
@@ @@
Array &0 (
- 0 => 'provider1'
+ 0 => 'provider2'
1 => 'first'
2 => 'second'
)
/home/sb/DependencyAndDataProviderComboTest.php:32
FAILURES!
Tests: 4, Assertions: 4, Failures: 1.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class DataTest extends TestCase
{
/**
* @dataProvider additionWithNonNegativeNumbersProvider
* @dataProvider additionWithNegativeNumbersProvider
*/
public function testAdd(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $a + $b);
}
public function additionWithNonNegativeNumbersProvider(): void
{
return [
[0, 1, 1],
[1, 0, 1],
[1, 1, 3]
];
}
public function additionWithNegativeNumbersProvider(): array
{
return [
[-1, 1, 0],
[-1, -1, -2],
[1, -1, 0]
];
}
}
$ phpunit DataTest PHPUnit 9.5.0 by Sebastian Bergmann and contributors. ..F... 6 / 6 (100%) Time: 0 seconds, Memory: 5.75Mb There was 1 failure: 1) DataTest::testAdd with data set #3 (1, 1, 3) Failed asserting that 2 is identical to 3. /home/sb/DataTest.php:12 FAILURES! Tests: 6, Assertions: 6, Failures: 1.
Примечание
Когда тест зависит от теста, использующего поставщиков данных, зависящий тест будет выполняться, когда зависящий тест успешно пройдёт хотя бы для одного набора данных. Результат теста, использующего поставщиков данных, не может быть внедрён в зависящий тест.
Примечание
Все поставщики данных выполняются до вызова статического метода setUpBeforeClass() и первого вызова метода setUp(). Из-за этого вы не можете получить доступ к переменным, которые вы создали там, изнутри поставщика данных. Это необходимо, чтобы PHPUnit мог вычислить общее количество тестов.
Проверка исключений
Пример 2.11 демонстрирует использование метода expectException() для проверки, выбрасывает ли код под тестированием исключение.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ExceptionTest extends TestCase
{
public function testException(): void
{
$this->expectException(InvalidArgumentException::class);
}
}
$ phpunit ExceptionTest PHPUnit 9.5.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 4.75Mb There was 1 failure: 1) ExceptionTest::testException Failed asserting that exception of type "InvalidArgumentException" is thrown. FAILURES! Tests: 1, Assertions: 1, Failures: 1.
Помимо метода expectException(), существуют методы expectExceptionCode(), expectExceptionMessage(), и expectExceptionMessageMatches() для настройки ожиданий на исключения, генерируемые кодом под тестированием.
Примечание
Обратите внимание, что expectExceptionMessage() утверждает, что сообщение $actual содержит сообщение $expected, не выполняя точного сравнения строк.
Проверка ошибок, предупреждений и сообщений PHP
По умолчанию PHPUnit преобразует ошибки, предупреждения и сообщения PHP, которые возникают во время выполнения теста, в исключение. Это позволяет ожидать возникновения ошибки, предупреждения или сообщения PHP в тесте, как показано в примере 2.12.
Примечание
Настройка среды выполнения PHP error_reporting может ограничивать, какие ошибки PHPUnit преобразует в исключения. Если у вас возникнут проблемы с этой функцией, убедитесь, что PHP не настроен на подавление типа ошибки, который вас интересует.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ErrorTest extends TestCase
{
public function testDeprecationCanBeExpected(): void
{
$this->expectDeprecation();
// Optionally test that the message is equal to a string
$this->expectDeprecationMessage('foo');
// Or optionally test that the message matches a regular expression
$this->expectDeprecationMessageMatches('/foo/');
\trigger_error('foo', \E_USER_DEPRECATED);
}
public function testNoticeCanBeExpected(): void
{
$this->expectNotice();
// Optionally test that the message is equal to a string
$this->expectNoticeMessage('foo');
// Or optionally test that the message matches a regular expression
$this->expectNoticeMessageMatches('/foo/');
\trigger_error('foo', \E_USER_NOTICE);
}
public function testWarningCanBeExpected(): void
{
$this->expectWarning();
// Optionally test that the message is equal to a string
$this->expectWarningMessage('foo');
// Or optionally test that the message matches a regular expression
$this->expectWarningMessageMatches('/foo/');
\trigger_error('foo', \E_USER_WARNING);
}
public function testErrorCanBeExpected(): void
{
$this->expectError();
// Optionally test that the message is equal to a string
$this->expectErrorMessage('foo');
// Or optionally test that the message matches a regular expression
$this->expectErrorMessageMatches('/foo/');
\trigger_error('foo', \E_USER_ERROR);
}
}
При тестировании кода, использующего встроенные функции PHP, такие как fopen(), которые могут вызывать ошибки, иногда полезно использовать подавление ошибок во время тестирования. Это позволяет проверять возвращаемые значения, подавляя сообщения, которые привели бы к возникновению исключения обработчиком ошибок PHPUnit.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ErrorSuppressionTest extends TestCase
{
public function testFileWriting(): void
{
$writer = new FileWriter;
$this->assertFalse(@$writer->write('/is-not-writeable/file', 'stuff'));
}
}
final class FileWriter
{
public function write($file, $content)
{
$file = fopen($file, 'w');
if ($file === false) {
return false;
}
// ...
}
}
$ phpunit ErrorSuppressionTest PHPUnit 9.5.0 by Sebastian Bergmann and contributors. . Time: 1 seconds, Memory: 5.25Mb OK (1 test, 1 assertion)
Без подавления ошибок тест потерпел бы неудачу, сообщая fopen(/is-not-writeable/file): failed to open stream: No such file or directory.
Проверка вывода
Иногда необходимо убедиться, что выполнение метода, например, генерирует ожидаемый вывод (например, с помощью echo или print). Класс PHPUnit\Framework\TestCase использует буферизацию вывода PHP для обеспечения необходимой функциональности.
Пример 2.14 показывает, как использовать метод 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';
}
}
$ phpunit OutputTest PHPUnit 9.5.0 by Sebastian Bergmann and contributors. .F Time: 0 seconds, Memory: 5.75Mb There was 1 failure: 1) OutputTest::testExpectBarActualBaz Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -'bar' +'baz' FAILURES! Tests: 2, Assertions: 2, Failures: 1.
Таблица 2.1 показывает методы для проверки вывода.
| Метод | Значение |
|---|---|
void expectOutputRegex(string $regularExpression) |
Устанавливает ожидание, что вывод соответствует $regularExpression. |
void expectOutputString(string $expectedString) |
Устанавливает ожидание, что вывод равен $expectedString. |
bool setOutputCallback(callable $callback) |
Устанавливает обратный вызов, который используется, например, для нормализации фактического вывода. |
string getActualOutput() |
Получить фактический вывод. |
Примечание
Тест, который выводит данные, потерпит неудачу в строгом режиме.
Вывод ошибок
В случае неудачи теста 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]
);
}
}
$ phpunit ArrayDiffTest
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) ArrayDiffTest::testEquality
Failed asserting that two arrays are identical.
--- Expected
+++ Actual
@@ @@
Array (
0 => 1
1 => 2
- 2 => 3
+ 2 => 33
3 => 4
4 => 5
5 => 6
)
/home/sb/ArrayDiffTest.php:7
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]
);
}
}
$ phpunit LongArrayDiffTest
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
F
Time: 0 seconds, Memory: 5.25Mb
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
)
/home/sb/LongArrayDiffTest.php:7
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]
);
}
}
$ phpunit ArrayWeakComparisonTest
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
F
Time: 0 seconds, Memory: 5.25Mb
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
)
/home/sb/ArrayWeakComparisonTest.php:7
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
В этом примере различие в первом индексе между 1 и '1' сообщается, даже если assertEquals() считает значения совпадающими.
© 2005–2020 Sebastian Bergmann
Licensed under the Creative Commons Attribution 3.0 Unported License.
https://phpunit.readthedocs.io/en/9.5/writing-tests-for-phpunit.html