Юнит-тестирование для чайников

Testing PHP Errors, Warnings, and Notices

By default, PHPUnit converts PHP errors, warnings, and notices that are triggered during the execution of a test to an exception. Among other benefits, this makes it possible to expect that a PHP error, warning, or notice is triggered in a test as shown in .

Note


PHP’s runtime configuration can limit which errors PHPUnit will convert to exceptions. If you are having issues with this feature, be sure PHP is not configured to suppress the type of error you are interested in.

Example 2.12 Expecting PHP errors, warnings, and notices

<?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);
    }
}

When testing code that uses PHP built-in functions such as that may trigger errors it can sometimes be useful to use error suppression while testing. This allows you to check the return values by suppressing notices that would lead to an exception raised by PHPUnit’s error handler.

Example 2.13 Testing return values of code that uses PHP Errors

<?php
use PHPUnit\Framework\TestCase;

class ErrorSuppressionTest extends TestCase
{
    public function testFileWriting()
    {
        $writer = new FileWriter;

        $this->assertFalse(@$writer->write('/is-not-writeable/file', 'stuff'));
    }
}

class FileWriter
{
    public function write($file, $content)
    {
        $file = fopen($file, 'w');

        if ($file == false) {
            return false;
        }

        // ...
    }
}
$ phpunit ErrorSuppressionTest
PHPUnit 9.2.0 by Sebastian Bergmann and contributors.

.

Time: 1 seconds, Memory: 5.25Mb

OK (1 test, 1 assertion)

Имитация файловой системы (УСТАРЕЛО)

vfsStream — обёртка потока для виртуальной файловой системы, которая может быть полезной в модульных тестах для имитации реальной файловой системы.

Просто добавьте зависимость в файл вашего проекта, если вы используете Composer для управления зависимостями в своём проекте. Вот самый минимальный файл , который просто определяет зависимости для разработки PHPUnit 4.6 и vfsStream:

{
    "require-dev" {
        "phpunit/phpunit" "~4.6",
        "mikey179/vfsstream" "~1"
    }
}

показывает класс, взаимодействующий с файловой системой.

Пример 9.20 Класс, взаимодействующий с файловой системой

<?php
use PHPUnit\Framework\TestCase;

class Example
{
    protected $id;
    protected $directory;

    public function __construct($id)
    {
        $this->id = $id;
    }

    public function setDirectory($directory)
    {
        $this->directory = $directory . DIRECTORY_SEPARATOR . $this->id;

        if (!file_exists($this->directory)) {
            mkdir($this->directory, 0700, true);
        }
    }
}

Без виртуальной файловой системы, такой как vfsStream, мы не можем протестировать метод в изоляции от внешнего воздействия (см. ).

Пример 9.21 Тестирование класса, взаимодействующего с файловой системой

<?php
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    protected function setUp() void
    {
        if (file_exists(dirname(__FILE__) . '/id')) {
            rmdir(dirname(__FILE__) . '/id');
        }
    }

    public function testDirectoryIsCreated()
    {
        $example = new Example('id');
        $this->assertFalse(file_exists(dirname(__FILE__) . '/id'));

        $example->setDirectory(dirname(__FILE__));
        $this->assertTrue(file_exists(dirname(__FILE__) . '/id'));
    }

    protected function tearDown() void
    {
        if (file_exists(dirname(__FILE__) . '/id')) {
            rmdir(dirname(__FILE__) . '/id');
        }
    }
}

Приведённый выше подход имеет несколько недостатков:

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

показывает, как vfsStream может использоваться для имитации файловой системы в тесте для класса, который взаимодействует с файловой системой.

Пример 9.22 Имитация файловой системы в тесте для класса, взаимодействующего с файловой системой

<?php
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    public function setUp() void
    {
        vfsStreamWrapper::register();
        vfsStreamWrapper::setRoot(new vfsStreamDirectory('exampleDir'));
    }

    public function testDirectoryIsCreated()
    {
        $example = new Example('id');
        $this->assertFalse(vfsStreamWrapper::getRoot()->hasChild('id'));

        $example->setDirectory(vfsStream::url('exampleDir'));
        $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('id'));
    }
}

Это имеет ряд преимуществ:

  • Тест сам стал более кратким.
  • vfsStream даёт разработчику теста полный контроль над тем, как выглядит окружение файловой системы для тестируемого кода.
  • Поскольку операции файловой системы больше не выполняются на реальной файловой системе, операции очистки в методе больше не требуются.

对异常进行测试

标注来测试被测代码中是否抛出了异常。

Example 2.10 使用 expectException() 方法

<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    public function testException()
    {
        $this->expectException(InvalidArgumentException::class);
    }
}
?>
$ phpunit ExceptionTest
PHPUnit 7.0.0 by Sebastian Bergmann and contributors.

F

Time:  seconds, Memory: 4.75Mb

There was 1 failure:

1) ExceptionTest::testException
Expected exception InvalidArgumentException

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

除了 方法外,还有 、 和 方法可以用于为被测代码所抛出的异常建立预期。

或者,也可以用 、、 和 标注来为被测代码所抛出的异常建立预期。:numref:writing-tests-for-phpunit.exceptions.examples.ExceptionTest2.php`展示了一个范例。


Example 2.11 使用 @expectedException 标注

<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    /**
     * @expectedException InvalidArgumentException
     */
    public function testException()
    {
    }
}
?>

Глобальное состояние

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

В PHP глобальные переменные работают следующим образом:

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

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

До версии 6, PHPUnit по умолчанию запускал тесты таким образом, что изменения в глобальных и суперглобальных переменных (, , , , , , , ) не влияли на другие тесты.

Начиная с версии 6, PHPUnit больше не делает операции резервного копирования и восстановления глобальных и суперглобальных переменных по умолчанию. Это можно включить, используя опцию или настройку в конфигурационном XML-файле.

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

Примечание

Операции резервного копирования и восстановления глобальных переменных и статических атрибутов классов используют и .

Объекты некоторых классов (например, ) не могут быть сериализованы, и операция резервного копирования будет прервана, когда подобный объект будет сохраняться, например, в массив .

Аннотация , которая обсуждается в , может использоваться для управления операциями резервного копирования и восстановления глобальных переменных. Кроме этого, вы можете предоставить чёрный список глобальных переменных, которые должны быть исключены при выполнении операций резервного копирования и восстановления, как показано ниже:

class MyTest extends TestCase
{
    protected $backupGlobalsBlacklist = 'globalVariable'];

    // ...
}

Примечание

Установка свойства внутри, например, метода , не даст никакого эффекта.

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

Она обрабатывает все классы, объявленные в момент запуска теста, а не только сам тестовый класс. Она применяется только к статическим свойствам класса, а не к статическим переменным внутри функций.

Примечание

Операция выполняется перед каждым тестовым методом, но только если она включена. Если статическое значение было изменено ранее выполненным тестом с отключенным , тогда это значение будет скопировано и восстановлено, но не к первоначальному значению по умолчанию. PHP не записывает первоначально объявленное значение по умолчанию любой статической переменной.

То же самое относительно и к статическим свойствам классов, которые недавно были загружены или объявлены внутри теста. Они не могут быть сброшены к первоначально объявленному значению по умолчанию после теста, так как это значение неизвестно. Независимо установленного значения, произойдёт утечка памяти в последующие тесты.

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

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

class MyTest extends TestCase
{
    protected $backupStaticAttributesBlacklist = 
        'className' => 'attributeName'
    ];

    // ...
}

Примечание

Установка свойства внутри, например, метода , не даст никакого эффекта.

Определение покрытых методов

Аннотация (см. appendixes.annotations.covers.tables.annotations) может использоваться в тестовом коде для указания, какие методы тестовый метод хочет протестировать. Если она указана, то в информации о покрытии кода будут будут только эти указанные методы.

показывает это на примере.

Пример 10.2 Тесты, в которых указывается, какой метод они хотят покрыть

<?php
use PHPUnit\Framework\TestCase;

class BankAccountTest extends TestCase
{
    protected $ba;

    protected function setUp() void
    {
        $this->ba = new BankAccount;
    }

    /**
     * @covers BankAccount::getBalance
     */
    public function testBalanceIsInitiallyZero()
    {
        $this->assertSame(, $this->ba->getBalance());
    }

    /**
     * @covers BankAccount::withdrawMoney
     */
    public function testBalanceCannotBecomeNegative()
    {
        try {
            $this->ba->withdrawMoney(1);
        }

        catch (BankAccountException $e) {
            $this->assertSame(, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    /**
     * @covers BankAccount::depositMoney
     */
    public function testBalanceCannotBecomeNegative2()
    {
        try {
            $this->ba->depositMoney(-1);
        }

        catch (BankAccountException $e) {
            $this->assertSame(, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    /**
     * @covers BankAccount::getBalance
     * @covers BankAccount::depositMoney
     * @covers BankAccount::withdrawMoney
     */
    public function testDepositWithdrawMoney()
    {
        $this->assertSame(, $this->ba->getBalance());
        $this->ba->depositMoney(1);
        $this->assertSame(1, $this->ba->getBalance());
        $this->ba->withdrawMoney(1);
        $this->assertSame(, $this->ba->getBalance());
    }
}

Также можно указать, что тест не должен покрывать какой-либо метод, используя аннотацию (см. ). Это может быть полезно при написании интеграционных тестов, чтобы убедиться, что вы только генерируете покрытие кода с помощью модульных тестов.

Testing Output

Sometimes you want to assert that the execution of a method, for instance, generates an expected output (via or , for example). The class uses PHP’s Output Buffering feature to provide the functionality that is necessary for this.

shows how to use the method to set the expected output. If this expected output is not generated, the test will be counted as a failure.

Example 2.14 Testing the output of a function or method

<?php
use PHPUnit\Framework\TestCase;

class OutputTest extends TestCase
{
    public function testExpectFooActualFoo()
    {
        $this->expectOutputString('foo');
        print 'foo';
    }

    public function testExpectBarActualBaz()
    {
        $this->expectOutputString('bar');
        print 'baz';
    }
}
$ phpunit OutputTest
PHPUnit 9.2.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.

shows the methods provided for testing output

Table 2.1 Methods for testing output
Method Meaning
Set up the expectation that the output matches a .
Set up the expectation that the output is equal to an .
Sets up a callback that is used to, for instance, normalize the actual output.
Get the actual output.

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

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

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

показывает, как использовать аннотацию для представления зависимостей между тестовыми методами.

Пример 2.2 Использование аннотации для описания зависимостей

<?php
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    public function testEmpty()
    {
        $stack = [];
        $this->assertEmpty($stack);

        return $stack;
    }

    /**
     * @depends testEmpty
     */
    public function testPush(array $stack)
    {
        array_push($stack, 'foo');
        $this->assertSame('foo', $stackcount($stack)-1]);
        $this->assertNotEmpty($stack);

        return $stack;
    }

    /**
     * @depends testPush
     */
    public function testPop(array $stack)
    {
        $this->assertSame('foo', array_pop($stack));
        $this->assertEmpty($stack);
    }
}

В вышеприведённом примере первый тест, , создаёт новый массив и утверждает, что он пуст. Затем тест возвращает фикстуру в качестве результата. Второй тест, , зависит от и ему передаётся результат этого зависимого теста в качестве аргумента. Наконец, зависит от .

Примечание

Возвращаемое значение, предоставленное поставщиком, по умолчанию передаётся потребителям «как есть». Это означает, что когда поставщик возвращает объект, ссылка на этот объект передаётся потребителям. Вместо ссылки возможна, либо (а) (глубокая) копия через или (б) (поверхностная) копия (на основе ключевого слова PHP ) через .

Чтобы быстро находить дефекты, нам нужно сосредоточить внимание на соответствующих неудачных тестах. Вот почему PHPUnit пропускает выполнение теста, когда зависимый тест (тест с зависимостью) провалился (не прошёл). Это помогает локализовать дефекты за счёт использования зависимостей между тестами, как это показано в


Пример 2.3 Использование зависимостей между тестами

<?php
use PHPUnit\Framework\TestCase;

class DependencyFailureTest extends TestCase
{
    public function testOne()
    {
        $this->assertTrue(false);
    }

    /**
     * @depends testOne
     */
    public function testTwo()
    {
    }
}
$ phpunit --verbose DependencyFailureTest
PHPUnit latest.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.

У теста может быть несколько аннотаций . PHPUnit не изменяет порядок выполнения тестов, поэтому вы должны убедиться, что все зависимости действительно могут быть выполнены до запуска теста.

Тест, содержащий более одной аннотации , получит фикстуру от первого поставщика в качестве первого аргумента, фикстуру от второго поставщика вторым аргументом и т.д. См.

Пример 2.4 Тест с несколькими зависимостями

<?php
use PHPUnit\Framework\TestCase;

class MultipleDependenciesTest extends TestCase
{
    public function testProducerFirst()
    {
        $this->assertTrue(true);
        return 'first';
    }

    public function testProducerSecond()
    {
        $this->assertTrue(true);
        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     */
    public function testConsumer($a, $b)
    {
        $this->assertSame('first', $a);
        $this->assertSame('second', $b);
    }
}

Test Dependencies

PHPUnit supports the declaration of explicit dependencies between test methods. Such dependencies do not define the order in which the test methods are to be executed but they allow the returning of an instance of the test fixture by a producer and passing it to the dependent consumers.

  • A producer is a test method that yields its unit under test as return value.
  • A consumer is a test method that depends on one or more producers and their return values.

shows how to use the annotation to express dependencies between test methods.

Example 2.2 Using the annotation to express dependencies

<?php
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    public function testEmpty()
    {
        $stack = [];
        $this->assertEmpty($stack);

        return $stack;
    }

    /**
     * @depends testEmpty
     */
    public function testPush(array $stack)
    {
        array_push($stack, 'foo');
        $this->assertSame('foo', $stackcount($stack)-1]);
        $this->assertNotEmpty($stack);

        return $stack;
    }

    /**
     * @depends testPush
     */
    public function testPop(array $stack)
    {
        $this->assertSame('foo', array_pop($stack));
        $this->assertEmpty($stack);
    }
}

In the example above, the first test, , creates a new array and asserts that it is empty. The test then returns the fixture as its result. The second test, , depends on and is passed the result of that depended-upon test as its argument. Finally, depends upon .

Note

The return value yielded by a producer is passed “as-is” to its consumers by default. This means that when a producer returns an object, a reference to that object is passed to the consumers. Instead of a reference either (a) a (deep) copy via , or (b) a (normal shallow) clone (based on PHP keyword ) via are possible too.

To quickly localize defects, we want our attention to be focussed on relevant failing tests. This is why PHPUnit skips the execution of a test when a depended-upon test has failed. This improves defect localization by exploiting the dependencies between tests as shown in .

Example 2.3 Exploiting the dependencies between tests

<?php
use PHPUnit\Framework\TestCase;

class DependencyFailureTest extends TestCase
{
    public function testOne()
    {
        $this->assertTrue(false);
    }

    /**
     * @depends testOne
     */
    public function testTwo()
    {
    }
}
$ phpunit --verbose DependencyFailureTest
PHPUnit 9.2.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.

A test may have more than one annotation. PHPUnit does not change the order in which tests are executed, you have to ensure that the dependencies of a test can actually be met before the test is run.

A test that has more than one annotation will get a fixture from the first producer as the first argument, a fixture from the second producer as the second argument, and so on. See

Example 2.4 Test with multiple dependencies

<?php
use PHPUnit\Framework\TestCase;

class MultipleDependenciesTest extends TestCase
{
    public function testProducerFirst()
    {
        $this->assertTrue(true);
        return 'first';
    }

    public function testProducerSecond()
    {
        $this->assertTrue(true);
        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     */
    public function testConsumer($a, $b)
    {
        $this->assertSame('first', $a);
        $this->assertSame('second', $b);
    }
}

Расширение TestRunner

PHPUnit latest поддерживает расширения TestRunner, которые привязываются к различным событиям во время выполнения теста. См. для получения дополнительной информации о регистрации расширений в конфигурационном XML-файле PHPUnit.

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

Интерфейсы доступных событий

показывает пример расширения, реализующего и :

Пример 12.6 Пример расширения TestRunner

<?php

namespace Vendor;

use PHPUnit\Runner\AfterLastTestHook;
use PHPUnit\Runner\BeforeFirstTestHook;

final class MyExtension implements BeforeFirstTestHook, AfterLastTestHook
{
    public function executeAfterLastTest() void
    {
        // вызывается после последнего выполненного теста
    }

    public function executeBeforeFirstTest() void
    {
        // вызывается до выполнения первого теста
    }
}

测试的依赖关系

PHPUnit支持对测试方法之间的显式依赖关系进行声明。这种依赖关系并不是定义在测试方法的执行顺序中,而是允许生产者(producer)返回一个测试基境(fixture)的实例,并将此实例传递给依赖于它的消费者(consumer)们。

  • 生产者(producer),是能生成被测单元并将其作为返回值的测试方法。
  • 消费者(consumer),是依赖于一个或多个生产者及其返回值的测试方法。

展示了如何用 标注来表达测试方法之间的依赖关系。

Example 2.2 用 标注来表达依赖关系

<?php
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    public function testEmpty()
    {
        $stack = [];
        $this->assertEmpty($stack);

        return $stack;
    }

    /**
     * @depends testEmpty
     */
    public function testPush(array $stack)
    {
        array_push($stack, 'foo');
        $this->assertEquals('foo', $stackcount($stack)-1]);
        $this->assertNotEmpty($stack);

        return $stack;
    }

    /**
     * @depends testPush
     */
    public function testPop(array $stack)
    {
        $this->assertEquals('foo', array_pop($stack));
        $this->assertEmpty($stack);
    }
}
?>

在上例中,第一个测试, ,创建了一个新数组,并断言其为空。随后,此测试将此基境作为结果返回。第二个测试,,依赖于 ,并将所依赖的测试之结果作为参数传入。最后, 依赖于 。

Note


默认情况下,生产者所产生的返回值将“原样”传递给相应的消费者。这意味着,如果生产者返回的是一个对象,那么传递给消费者的将是一个指向此对象的引用。如果需要传递对象的副本而非引用,则应当用 @depends clone 替代 @depends。

为了快速定位缺陷,我们希望把注意力集中于相关的失败测试上。这就是为什么当某个测试所依赖的测试失败时,PHPUnit 会跳过这个测试。通过利用测试之间的依赖关系,缺陷定位得到了改进,如 中所示。

Example 2.3 利用测试之间的依赖关系

<?php
use PHPUnit\Framework\TestCase;

class DependencyFailureTest extends TestCase
{
    public function testOne()
    {
        $this->assertTrue(false);
    }

    /**
     * @depends testOne
     */
    public function testTwo()
    {
    }
}
?>
$ phpunit --verbose DependencyFailureTest
PHPUnit 7.0.0 by Sebastian Bergmann and contributors.

FS

Time:  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.

测试可以使用多个 标注。PHPUnit 不会更改测试的运行顺序,因此你需要自行保证某个测试所依赖的所有测试均出现于这个测试之前。

拥有多个 标注的测试,其第一个参数是第一个生产者提供的基境,第二个参数是第二个生产者提供的基境,以此类推。参见

Example 2.4 有多重依赖的测试

<?php
use PHPUnit\Framework\TestCase;

class MultipleDependenciesTest extends TestCase
{
    public function testProducerFirst()
    {
        $this->assertTrue(true);
        return 'first';
    }

    public function testProducerSecond()
    {
        $this->assertTrue(true);
        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     */
    public function testConsumer()
    {
        $this->assertEquals(
            'first', 'second'],
            func_get_args()
        );
    }
}
?>

Пропуск тестов с помощью @requires

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

Таблица 7.3 Возможные примеры использования @requires
Тип Возможные значения Примеры Дополнительный пример
Любой идентификатор версии PHP @requires PHP 5.3.3 @requires PHP 7.1-dev
Любой идентификатор версии PHPUnit @requires PHPUnit 3.6.3 @requires PHPUnit 4.6
Регулярное выражения для @requires OS Linux @requires OS WIN32|WINNT
Любое @requires OSFAMILY Solaris @requires OSFAMILY Windows
Любой корректный параметр для function_exists @requires function imap_open @requires function ReflectionMethod::setAccessible
Имя расширения вместе с необязательным идентификатором версии @requires extension mysqli @requires extension redis 2.2.0

Пример 7.3 Пропуск тестового класса с использованием @requires

<?php
use PHPUnit\Framework\TestCase;

/**
 * @requires extension mysqli
 */
class DatabaseTest extends TestCase
{
    /**
     * @requires PHP 5.3
     */
    public function testConnection()
    {
        // Тест требует расширения mysqli и PHP >= 5.3
    }

    // ... Все остальные тесты требует расширения mysqli
}

Если вы используете синтаксис, который не компилируется с определённой версией PHP, посмотрите на версии, от которых зависят тестовые классы в XML-конфигурации (см )

Написание пользовательских утверждений

При написании пользовательских утверждений лучше всего следовать принципам реализации собственных утверждений PHPUnit. Как вы можете видеть в , метод — это просто обёртка над методами и : создаёт объект сопоставления, который передаётся для проведения вычисления.

Пример 12.1 Методы assertTrue() и isTrue() класса PHPUnit\Framework\Assert

<?php
namespace PHPUnit\Framework;

use PHPUnit\Framework\TestCase;

abstract class Assert
{
    // ...

    /**
     * Утверждает, что условие истинно
     *
     * @param  boolean $condition
     * @param  string  $message
     * @throws PHPUnit\Framework\AssertionFailedError
     */
    public static function assertTrue($condition, $message = '')
    {
        self::assertThat($condition, self::isTrue(), $message);
    }

    // ...

    /**
     * Возвращает объект сопоставления PHPUnit\Framework\Constraint\IsTrue.
     *
     * @return PHPUnit\Framework\Constraint\IsTrue
     * @since  Method available since Release 3.3.0
     */
    public static function isTrue()
    {
        return new PHPUnit\Framework\Constraint\IsTrue;
    }

    // ...
}

показывает, как наследует абстрактный базовый класс для объектов сопоставления (или ограничений), .

Пример 12.2 Класс PHPUnit\Framework\Constraint\IsTrue

<?php
namespace PHPUnit\Framework\Constraint;

use PHPUnit\Framework\Constraint;

class IsTrue extends Constraint
{
    /**
     * Вычисляет ограничение для параметра $other. Возвращает true, если
     * ограничение удовлетворяется, в противном случае — false.
     *
     * @param mixed $other Значение или объект для вычисления
     * @return bool
     */
    public function matches($other)
    {
        return $other === true;
    }

    /**
     * Возвращает ограничения в виде строки
     *
     * @return string
     */
    public function toString()
    {
        return 'это true';
    }
}

API утверждений базы данных

Инструмент тестирования расширения базы данных, безусловно, содержит утверждения, которые вы можете использовать для проверки текущего состояния базы данных, таблиц и подсчёта строк таблиц. В этом разделе подробно описывается эта функциональность:

Утверждение количество строк таблицы

Часто бывает полезно проверить, содержит ли таблица определённое количество строк. Вы можете легко достичь этого без дополнительного кода, используя API Connection. Предположим, мы хотим проверить, что после вставки строк в нашу гостевую книгу мы имеем не только две первоначальные записи, которые были во всех предыдущих примерах, но а также третью, только что добавленную:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class GuestbookTest extends TestCase
{
    use TestCaseTrait;

    public function testAddEntry()
    {
        $this->assertSame(2, $this->getConnection()->getRowCount('guestbook'), "Pre-Condition");

        $guestbook = new Guestbook();
        $guestbook->addEntry("suzy", "Hello world!");

        $this->assertSame(3, $this->getConnection()->getRowCount('guestbook'), "Inserting failed");
    }
}

Утверждение состояния таблицы

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

Для этого нам нужно определить экземпляр таблицы запроса (Query Table), который выводит содержимое по имени таблицы и SQL-запроса и сравнивает его с набором данных на основе файлов/массивов:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class GuestbookTest extends TestCase
{
    use TestCaseTrait;

    public function testAddEntry()
    {
        $guestbook = new Guestbook();
        $guestbook->addEntry("suzy", "Hello world!");

        $queryTable = $this->getConnection()->createQueryTable(
            'guestbook', 'SELECT * FROM guestbook'
        );
        $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml")
                              ->getTable("guestbook");
        $this->assertTablesEqual($expectedTable, $queryTable);
    }
}

Теперь для этого утверждения мы должны создать обычный XML-файл expectedBook.xml:

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Привет, дружище!" user="joe" created="2010-04-24 17:15:23" />
    <guestbook id="2" content="Мне нравится это!" user="nancy" created="2010-04-26 12:14:20" />
    <guestbook id="3" content="Привет, мир!" user="suzy" created="2010-05-01 21:47:08" />
</dataset>

Это утверждение будет успешным только в том случае, если оно будет запущено точно в 2010–05–01 21:47:08. Даты представляют собой особую проблему при тестировании с использованием базы данных, и мы может обойти эту ошибку, опуская столбец «created» в утверждении.

Скорректированный файл Flat XML expectedBook.xml, вероятно, теперь должен выглядеть следующим образом для прохождения утверждения:

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Привет, дружище!" user="joe" />
    <guestbook id="2" content="Мне нравится это!" user="nancy" />
    <guestbook id="3" content="Привет, мир!" user="suzy" />
</dataset>

Мы должны исправить вызов таблицы запроса (Query Table):

<?php
$queryTable = $this->getConnection()->createQueryTable(
    'guestbook', 'SELECT id, content, user FROM guestbook'
);

Утверждение результата запроса

Вы также можете утверждать результат сложных запросов с помощью подхода Query Table, просто указав имя результата с запросом и сравнивая его с набором данным:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class ComplexQueryTest extends TestCase
{
    use TestCaseTrait;

    public function testComplexQuery()
    {
        $queryTable = $this->getConnection()->createQueryTable(
            'myComplexQuery', 'SELECT complexQuery...'
        );
        $expectedTable = $this->createFlatXmlDataSet("complexQueryAssertion.xml")
                              ->getTable("myComplexQuery");
        $this->assertTablesEqual($expectedTable, $queryTable);
    }
}

С этим читают