Spec-Zone.ru › PHPUnit 8

Написание тестов для 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 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.

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

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

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

Пример 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 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.
Пример 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 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.
Пример 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 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.
Пример 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 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() для проверки, что код, который тестируется, генерирует исключение.

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

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

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

Таблица 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 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 разделит его и предоставит несколько строк контекста вокруг каждого различия.

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

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

Spec-Zone.ru

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