Spec-Zone.ru › PHPUnit 9

Написание тестов для PHPUnit

Пример 2.1 демонстрирует, как писать тесты с помощью PHPUnit для проверки операций с массивами в PHP. Пример представляет основные соглашения и шаги для написания тестов с PHPUnit:

  1. Тесты для класса Class находятся в классе ClassTest.

  2. ClassTest (в большинстве случаев) наследуется от PHPUnit\Framework\TestCase.

  3. Тесты — это публичные методы, которые имеют имена test*.

    В качестве альтернативы можно использовать аннотацию @test в блоке документации метода, чтобы отметить его как метод теста.

  4. Внутри методов тестов используются методы утверждения, такие как assertSame() (см. Утверждения), чтобы утверждать, что фактическое значение соответствует ожидаемому.

Пример 2.1 Тестирование операций с массивами с помощью PHPUnit
<?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));
    }
}
Мартин Фаулер:
Всякий раз, когда вы хотите ввести что-либо в оператор print или выражение отладчика, напишите это в виде теста вместо этого.

Зависимости тестов

Адриан Кун и др.:

Тесты юнит-уровня в первую очередь предназначены для помощи разработчикам в выявлении и исправлении ошибок, рефакторинге кода и служат документацией для единицы программного обеспечения, подвергающейся тестированию. Для достижения этих преимуществ тесты юнит-уровня должны, по идее, охватывать все возможные пути в программе. Обычно один тест юнит-уровня покрывает один конкретный путь в одной функции или методе. Однако метод теста не обязательно является обособленной, независимой сущностью. Часто между методами тестов существуют неявные зависимости, скрытые в сценарии реализации теста.

PHPUnit поддерживает объявление явных зависимостей между методами тестов. Такие зависимости не определяют порядок выполнения методов тестов, но они позволяют возвращать экземпляр фикстуры теста производителем и передавать его зависимым потребителям.

  • Производитель — это метод теста, который возвращает свою единицу тестирования как возвращаемое значение.
  • Потребитель — это метод теста, который зависит от одного или нескольких производителей и их возвращаемых значений.

Пример 2.2 демонстрирует использование аннотации @depends для выражения зависимостей между методами тестов.

Пример 2.2 Использование аннотации @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.

Пример 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

Пример 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, и возвращать массив на каждом шаге итерации. Для каждого массива, который является частью набора, метод теста вызывается с содержимым массива в качестве аргументов.

Пример 2.5 Использование поставщика данных, возвращающего массив массивов
<?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.

При использовании большого количества наборов данных полезно давать каждому имя с помощью строкового ключа вместо стандартного числового. Вывод будет более подробным, так как он будет содержать название набора данных, который приводит к ошибке теста.

Пример 2.6 Использование поставщика данных с именованными наборами данных
<?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.
Пример 2.7 Использование поставщика данных, возвращающего объект 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(): 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.
Пример 2.8 Класс CsvFileIterator
<?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

Пример 2.9 Комбинирование @depends и @dataProvider в одном тесте
<?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.
Пример 2.10 Использование нескольких поставщиков данных для одного теста
<?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() для проверки, выбрасывает ли код под тестированием исключение.

Пример 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 не настроен на подавление типа ошибки, который вас интересует.

Пример 2.12 Ожидание ошибок, предупреждений и сообщений 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.

Пример 2.13 Проверка возвращаемых значений кода, использующего ошибки PHP
<?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() для задания ожидаемого вывода. Если этот ожидаемый вывод не сгенерирован, тест будет считаться неудачным.

Пример 2.14 Проверка вывода функции или метода
<?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 показывает методы для проверки вывода.

Таблица 2.1 Методы проверки вывода
Метод Значение
void expectOutputRegex(string $regularExpression) Устанавливает ожидание, что вывод соответствует $regularExpression.
void expectOutputString(string $expectedString) Устанавливает ожидание, что вывод равен $expectedString.
bool setOutputCallback(callable $callback) Устанавливает обратный вызов, который используется, например, для нормализации фактического вывода.
string getActualOutput() Получить фактический вывод.

Примечание

Тест, который выводит данные, потерпит неудачу в строгом режиме.

Вывод ошибок

В случае неудачи теста PHPUnit пытается предоставить как можно больше контекста, который поможет определить проблему.

Пример 2.15 Вывод ошибки, сгенерированный при ошибке сравнения массивов
<?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 разделит его и предоставит несколько строк контекста вокруг каждой разницы.

Пример 2.16 Вывод ошибки при сравнении длинного массива
<?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() или других функций «слабого» сравнения для массивов или объектов.

Пример 2.17 Крайний случай в генерации разницы при использовании слабого сравнения
<?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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API