Spec-Zone.ru › PHPUnit

Фикстуры

Тест обычно следует структуре «Подготовка, Действие, Проверка»: подготовка всех необходимых предварительных условий и входных данных (так называемой тестовой фикстуры), действие над тестируемым объектом и проверка того, что ожидаемые результаты получены.

Подготовка, Ожидание, Действие

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

Иногда тестовая фикстура состоит из одного объекта, а иногда, например, представляет собой более сложный граф объектов. Объём кода, необходимого для её создания, соответственно увеличивается. Суть самого теста теряется в массе кода для настройки тестовой фикстуры. Эта проблема усугубляется, когда вы пишете несколько тестов с похожими тестовыми фикстурами.

PHPUnit поддерживает повторное использование кода настройки в разных тестах. Перед запуском метода теста вызывается шаблонный метод с именем setUp(): именно здесь можно создать тестовую фикстуру. После завершения метода теста, независимо от того, прошёл он успешно или завершился неудачей, вызывается другой шаблонный метод с именем tearDown(): именно здесь можно очистить объекты, которые вы тестировали.

Пример 5.1 Пример класса теста, использующего setUp() и tearDown()
<?php declare(strict_types=1);
namespace example;

use PHPUnit\Framework\TestCase;

final class ExampleTest extends TestCase
{
    private ?Example $example;

    public function testSomething(): void
    {
        $this->assertSame(
            'the-result',
            $this->example->doSomething()
        );
    }

    protected function setUp(): void
    {
        $this->example = new Example(
            $this->createStub(Collaborator::class)
        );
    }

    protected function tearDown(): void
    {
        $this->example = null;
    }
}

Шаблонные методы setUp() и tearDown() запускаются для каждого метода теста (и для новых экземпляров) класса теста.

Шаблонные методы

setUp() и tearDown() — это два из шести шаблонных методов, которые PHPUnit вызывает на протяжении жизненного цикла класса теста. Ниже приведён полный порядок выполнения:

setUpBeforeClass()          Once before the first test of the class
├── setUp()                 Before each test
│   ├── assertPreConditions()
│   │   ├── test method
│   │   assertPostConditions()
│   tearDown()              After each test
tearDownAfterClass()        Once after the last test of the class

setUpBeforeClass() и tearDownAfterClass() вызываются один раз для всего класса теста (см. раздел Совместное использование фикстуры). Остальные четыре шаблонных метода вызываются для каждого метода теста. Методы assertPreConditions() и assertPostConditions() описаны ниже.

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

Одна из проблем шаблонных методов setUp() и tearDown() заключается в том, что они вызываются даже для тестов, не использующих тестовую фикстуру, управляемую этими методами, — в приведённом выше примере свойство $this->example.

Другая проблема может возникнуть при использовании наследования:

Пример 5.2 Пример абстрактного класса теста с методом setUp()
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

abstract class MyTestCase extends TestCase
{
    protected function setUp(): void
    {
        // ...
    }
}
Пример 5.3 Пример конкретного класса теста, расширяющего абстрактный класс теста с методом setUp()
<?php declare(strict_types=1);
namespace example;

use PHPUnit\Framework\TestCase;

final class ExampleTest extends MyTestCase
{
    protected function setUp(): void
    {
        // ...
    }
}

Если при реализации ExampleTest::setUp() забыть вызвать parent::setUp(), функциональность, предоставляемая MyTestCase, работать не будет. Чтобы снизить этот риск, доступны такие атрибуты, как PHPUnit\Framework\Attributes\Before и PHPUnit\Framework\Attributes\After. С их помощью можно настроить вызов нескольких методов до и после теста соответственно, не вызывая parent::setUp(). Подробности и примеры см. в разделе Использование атрибутов вместо шаблонных методов.

Настройка важнее очистки

В теории setUp() и tearDown() симметричны, но на практике это не так. Как правило, реализовывать tearDown() нужно только в том случае, если в setUp() вы выделили внешние ресурсы, например файлы или сокеты. Если вы не создаёте большие графы объектов в setUp() и не сохраняете их в свойствах объекта теста, то обычно можете не использовать tearDown().

Однако, если вы создаёте большие графы объектов в setUp() и сохраняете их в свойствах объекта теста, возможно, стоит unset() переменные, содержащие эти объекты, в tearDown(), чтобы сборщик мусора мог освободить их раньше.

Объекты, созданные в setUp() (или методах теста) и сохранённые в свойствах объекта теста, автоматически удаляются сборщиком мусора только в конце PHP-процесса, в котором выполняется PHPUnit.

Предварительные и постусловия

Шаблонный метод assertPreConditions() вызывается после setUp(), но до метода теста. Шаблонный метод assertPostConditions() вызывается после метода теста, но до tearDown().

Эти методы предназначены для проверок, общих для всех тестов класса теста. В отличие от setUp() и tearDown(), сбой в assertPreConditions() или assertPostConditions() регистрируется как провал теста (а не как ошибка), поэтому сообщение о сбое будет содержать подробности проверки.

Пример 5.4 Использование assertPreConditions() и assertPostConditions()
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class SomeTest extends TestCase
{
    private ?SomeObject $object;

    protected function setUp(): void
    {
        $this->object = new SomeObject;
    }

    protected function assertPreConditions(): void
    {
        $this->assertTrue($this->object->isReady());
    }

    protected function assertPostConditions(): void
    {
        $this->assertFalse($this->object->hasErrors());
    }

    protected function tearDown(): void
    {
        $this->object = null;
    }

    public function testSomething(): void
    {
        $this->object->doSomething();

        $this->assertSame('expected', $this->object->result());
    }
}

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

Использование атрибутов вместо шаблонных методов

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

Шаблонный метод

Атрибут

setUpBeforeClass()

BeforeClass

setUp()

Before

assertPreConditions()

PreCondition

assertPostConditions()

PostCondition

tearDown()

After

tearDownAfterClass()

AfterClass

Главное преимущество атрибутов перед шаблонными методами заключается в том, что для одной и той же фазы можно настроить несколько методов. Это особенно полезно в иерархиях классов: каждый класс может объявить собственные методы-перехватчики, не вызывая, например, parent::setUp().

Пример 5.5 Использование атрибутов #[Before] и #[After]
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\After;
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\TestCase;

abstract class MyTestCase extends TestCase
{
    #[Before]
    protected function setUpLogger(): void
    {
        // Set up logging for all tests ...
    }

    #[After]
    protected function tearDownLogger(): void
    {
        // Tear down logging after all tests ...
    }
}
Пример 5.6 Подклассу не нужно вызывать parent::setUp()
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\Before;

final class MyTest extends MyTestCase
{
    #[Before]
    protected function setUpFixture(): void
    {
        // Set up the fixture for this test class ...
    }

    public function testSomething(): void
    {
        // Both setUpLogger() and setUpFixture() have been called at this point
        // ...
    }
}

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

Пример 5.7 Управление порядком выполнения с помощью приоритетов
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\TestCase;

final class PriorityTest extends TestCase
{
    #[Before(priority: 2)]
    protected function connectToDatabase(): void
    {
        // This runs first (higher priority) ...
    }

    #[Before(priority: 1)]
    protected function seedDatabase(): void
    {
        // This runs second (lower priority) ...
    }
}

Совместное использование фикстуры

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

Хороший пример фикстуры, которой имеет смысл пользоваться в нескольких тестах, — подключение к базе данных: вы один раз подключаетесь к базе данных и повторно используете это подключение вместо создания нового для каждого теста. Так тесты выполняются быстрее.

Шаблонные методы setUpBeforeClass() и tearDownAfterClass() вызываются соответственно перед запуском первого теста класса теста и после запуска последнего теста этого класса.

Не используйте проверки в setUpBeforeClass() или tearDownAfterClass()

Обратите внимание: использование проверок в методах setUpBeforeClass() или tearDownAfterClass() приводит к неопределённому поведению. Единственная причина, по которой использование проверок в этих шаблонных методах не приводит к ошибке средства запуска тестов, состоит в том, что мы хотим избежать накладных расходов во время выполнения на определение того, вызывается ли из этих шаблонных методов метод проверки.

В примере 5.8 шаблонные методы setUpBeforeClass() и tearDownAfterClass() используются для подключения к базе данных перед первым тестом класса теста и отключения от неё после последнего теста этого класса соответственно.

Пример 5.8 Совместное использование фикстуры в тестах набора тестов
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class DatabaseTest extends TestCase
{
    private static $dbh;

    public static function setUpBeforeClass(): void
    {
        self::$dbh = new PDO('sqlite::memory:');
    }

    public static function tearDownAfterClass(): void
    {
        self::$dbh = null;
    }
}

Атрибуты BeforeClass и AfterClass обеспечивают ту же функциональность, что и setUpBeforeClass() и tearDownAfterClass(), но позволяют настроить несколько методов для одной и той же фазы с возможностью задать приоритеты (см. раздел Использование атрибутов вместо шаблонных методов).

Невозможно переоценить, насколько совместное использование фикстур в тестах снижает их ценность. В основе проблемы проектирования лежит слабая связанность объектов. Вы добьётесь лучших результатов, если решите исходную проблему проектирования, а затем напишете тесты с заглушками (см. раздел Тестовые дублёры), вместо того чтобы создавать зависимости между тестами во время выполнения и упускать возможность улучшить проектирование.

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

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

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

  • Глобальная переменная $foo = 'bar'; хранится в $GLOBALS['foo'] = 'bar';.

  • Переменная $GLOBALS является так называемой суперглобальной переменной.

  • Суперглобальные переменные — это встроенные переменные, всегда доступные во всех областях видимости.

  • В области видимости функции или метода можно получить доступ к глобальной переменной $foo, обратившись напрямую к $GLOBALS['foo'] или используя global $foo; для создания локальной переменной, ссылающейся на глобальную.

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

PHPUnit может запускать тесты в режиме, при котором изменения глобальных и суперглобальных переменных ($GLOBALS, $_ENV, $_POST, $_GET, $_COOKIE, $_SERVER, $_FILES, $_REQUEST) не влияют на другие тесты. Этот режим можно включить с помощью параметра --globals-backup или задав backupGlobals="true" в XML-файле конфигурации.

С помощью параметра --static-backup или настройки backupStaticProperties="true" в XML-файле конфигурации эту изоляцию можно распространить на статические свойства классов.

Примечание

Для резервного копирования и восстановления глобальных переменных и статических свойств классов используются serialize() и unserialize().

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

Атрибут PHPUnit\Framework\Attributes\BackupGlobals можно использовать для управления резервным копированием и восстановлением глобальных переменных.

Атрибут PHPUnit\Framework\Attributes\ExcludeGlobalVariableFromBackup можно использовать, чтобы исключить определённые глобальные переменные из операций резервного копирования и восстановления.

Атрибут PHPUnit\Framework\Attributes\BackupStaticProperties можно использовать для управления резервным копированием и восстановлением статических свойств классов. Это затрагивает все статические свойства всех объявленных классов: их значения сохраняются перед каждым тестом и восстанавливаются после него. Обрабатываются все классы, объявленные к моменту начала теста, а не только сам класс теста. Атрибут применяется только к статическим свойствам классов, но не к статическим переменным внутри функций.

Атрибут PHPUnit\Framework\Attributes\ExcludeStaticPropertyFromBackup можно использовать, чтобы исключить определённые статические свойства из операций резервного копирования и восстановления.

Примечание

Резервное копирование статических свойств классов выполняется перед методом теста, но только если оно включено. Если статическое значение изменил ранее выполненный тест без BackupStaticProperties(true), будет сохранено и восстановлено именно это значение, а не исходное значение, объявленное по умолчанию.

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

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

Атрибут WithEnvironmentVariable можно использовать, чтобы задавать или удалять переменные окружения на время выполнения теста. Переменные окружения задаются до вызова setUp() и восстанавливаются до исходных значений после завершения tearDown().

© 2005–2026 Sebastian Bergmann
Licensed under the Creative Commons Attribution 3.0 Unported License.
https://docs.phpunit.de/en/13.2/fixtures.html

Spec-Zone.ru

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