Написание тестов для 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 8.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 8.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 8.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 8.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 8.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 8.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 8.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 8.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(), и expectExceptionMessageRegExp() для настройки ожиданий по исключениям, генерируемым тестируемым кодом.
Примечание
Обратите внимание, что 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 8.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 8.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 8.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 8.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 8.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/8.5/writing-tests-for-phpunit.html