Фикстуры
Одной из самых трудоёмких частей написания тестов является написание кода для подготовки среды в известном состоянии и последующего возвращения её в исходное состояние по завершении теста. Это известное состояние называется фикстурой теста.
В Тестирование операций с массивами с помощью PHPUnit фикстурой являлся массив, хранящийся в переменной $stack. Однако чаще всего фикстура будет более сложной, чем простой массив, и объём кода, необходимого для её подготовки, соответственно увеличится. Фактическое содержимое теста теряется в шуме подготовки фикстуры. Эта проблема усугубляется при написании нескольких тестов с похожими фикстурами. Без какой-либо помощи со стороны фреймворка тестирования нам пришлось бы дублировать код, который подготавливает фикстуру для каждого написанного нами теста.
PHPUnit поддерживает совместное использование кода подготовки. Перед выполнением метода теста вызывается шаблонный метод setUp(). В setUp() создаются объекты, по отношению к которым будут выполняться тесты. После завершения выполнения метода теста, независимо от успеха или неудачи, вызывается другой шаблонный метод tearDown(). В tearDown() производится очистка объектов, по отношению к которым проводились тесты.
В Использование аннотации @depends для выражения зависимостей мы использовали отношения «производитель-потребитель» между тестами для совместного использования фикстуры. Это не всегда желательно или возможно. Пример 4.1 демонстрирует, как мы можем написать тесты StackTest таким образом, что не сама фикстура повторно используется, а код, который её создаёт. Сначала мы объявляем переменную экземпляра $stack, которую мы будем использовать вместо локальной переменной метода. Затем помещаем создание фикстуры array в метод setUp(). Наконец, удаляем избыточный код из методов тестов и используем недавно введённую переменную экземпляра $this->stack, вместо локальной переменной метода $stack, с методом проверки assertSame().
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StackTest extends TestCase
{
private $stack;
protected function setUp(): void
{
$this->stack = [];
}
public function testEmpty(): void
{
$this->assertTrue(empty($this->stack));
}
public function testPush(): void
{
array_push($this->stack, 'foo');
$this->assertSame('foo', $this->stack[count($this->stack)-1]);
$this->assertFalse(empty($this->stack));
}
public function testPop(): void
{
array_push($this->stack, 'foo');
$this->assertSame('foo', array_pop($this->stack));
$this->assertTrue(empty($this->stack));
}
}
Шаблонные методы setUp() и tearDown() выполняются один раз для каждого метода теста (и на новых экземплярах) класса тестового случая.
Кроме того, шаблонные методы setUpBeforeClass() и tearDownAfterClass() вызываются перед запуском первого теста класса тестового случая и после запуска последнего теста класса тестового случая соответственно.
Нижеприведённый пример показывает все шаблонные методы, доступные в классе тестового случая.
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class TemplateMethodsTest extends TestCase
{
public static function setUpBeforeClass(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function setUp(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function assertPreConditions(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
}
public function testOne(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
$this->assertTrue(true);
}
public function testTwo(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
$this->assertTrue(false);
}
protected function assertPostConditions(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function tearDown(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
}
public static function tearDownAfterClass(): void
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function onNotSuccessfulTest(Throwable $t): void
{
fwrite(STDOUT, __METHOD__ . "\n");
throw $t;
}
}
$ phpunit TemplateMethodsTest PHPUnit 9.5.0 by Sebastian Bergmann and contributors. TemplateMethodsTest::setUpBeforeClass TemplateMethodsTest::setUp TemplateMethodsTest::assertPreConditions TemplateMethodsTest::testOne TemplateMethodsTest::assertPostConditions TemplateMethodsTest::tearDown .TemplateMethodsTest::setUp TemplateMethodsTest::assertPreConditions TemplateMethodsTest::testTwo TemplateMethodsTest::tearDown TemplateMethodsTest::onNotSuccessfulTest FTemplateMethodsTest::tearDownAfterClass Time: 0 seconds, Memory: 5.25Mb There was 1 failure: 1) TemplateMethodsTest::testTwo Failed asserting that <boolean:false> is true. /home/sb/TemplateMethodsTest.php:30 FAILURES! Tests: 2, Assertions: 2, Failures: 1.
Больше setUp() чем tearDown()
setUp() и tearDown() теоретически симметричны, но не на практике. На практике вам нужно реализовать только tearDown(), если вы выделили внешние ресурсы, такие как файлы или сокеты, в setUp(). Если ваш setUp() просто создаёт обычные объекты PHP, вы можете обычно пропустить tearDown(). Однако, если вы создаёте много объектов в setUp(), вам может потребоваться unset() переменные, указывающие на эти объекты, в tearDown(), чтобы они могли быть собраны сборщиком мусора. Сбор мусора объектов тестового случая непредсказуем.
Варианты
Что происходит, когда у вас есть два теста с немного разными настройками? Есть два варианта:
- Если код
setUp()отличается только незначительно, переместите код, отличающийся от кодаsetUp(), в метод теста. - Если у вас действительно другая
setUp(), вам нужен другой класс тестового случая. Назовите класс в соответствии с различием в настройке.
Общий фикстура
Есть несколько веских причин для совместного использования фикстур между тестами, но в большинстве случаев необходимость совместного использования фикстуры между тестами вызвана нерешённой проблемой проектирования.
Хороший пример фикстуры, которую имеет смысл совместно использовать между несколькими тестами, — это подключение к базе данных: вы подключаетесь к базе данных один раз и повторно используете подключение к базе данных вместо создания нового подключения для каждого теста. Это ускоряет выполнение тестов.
Пример 4.3 использует шаблонные методы setUpBeforeClass() и tearDownAfterClass() для подключения к базе данных перед первым тестом класса тестового случая и для отключения от базы данных после последнего теста тестового случая соответственно.
<?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;
}
}
Нельзя достаточно подчеркнуть, что совместное использование фикстур между тестами снижает ценность тестов. Основная проблема заключается в том, что объекты слабо связаны. Вы добьётесь лучших результатов, решив основную проблему проектирования и затем написав тесты с использованием «заглушек» (см. Тестовые заглушки), чем создавая зависимости между тестами во время выполнения и игнорируя возможность улучшения своего дизайна.
Глобальное состояние
Трудно тестировать код, использующий синглтоны. То же самое относится к коду, использующему глобальные переменные. Как правило, код, который вы хотите протестировать, сильно связан с глобальной переменной, и вы не можете контролировать её создание. Дополнительной проблемой является тот факт, что изменение одной переменной тестируемым тестом может сломать другой тест.
В PHP глобальные переменные работают следующим образом:
- Глобальная переменная
$foo = 'bar';хранится как$GLOBALS['foo'] = 'bar';. - Переменная
$GLOBALS— это так называемая суперглобальная переменная. - Суперглобальные переменные — это встроенные переменные, которые всегда доступны во всех областях видимости.
- В области видимости функции или метода вы можете получить доступ к глобальной переменной
$foo, либо напрямую обратившись к$GLOBALS['foo'], либо используяglobal $foo;, чтобы создать локальную переменную со ссылкой на глобальную переменную.
Помимо глобальных переменных, статические атрибуты классов также являются частью глобального состояния.
До версии 6, по умолчанию, PHPUnit запускал ваши тесты таким образом, что изменения глобальных и суперглобальных переменных ($GLOBALS, $_ENV, $_POST, $_GET, $_COOKIE, $_SERVER, $_FILES, $_REQUEST) не влияли на другие тесты.
Начиная с версии 6, PHPUnit больше не выполняет эту операцию резервного копирования и восстановления для глобальных и суперглобальных переменных по умолчанию. Она может быть активирована с помощью опции --globals-backup или установки backupGlobals="true" в файле конфигурации XML.
Используя опцию --static-backup или установку backupStaticAttributes="true" в файле конфигурации XML, эта изоляция может быть распространена на статические атрибуты классов.
Примечание
Операции резервного копирования и восстановления для глобальных переменных и статических атрибутов класса используют serialize() и unserialize().
Объекты некоторых классов (например, PDO) не могут быть сериализованы, и операция резервного копирования будет прервана при хранении такого объекта, например, в массиве $GLOBALS.
Аннотация @backupGlobals, о которой говорится в @backupGlobals, может использоваться для управления операциями резервного копирования и восстановления для глобальных переменных. В качестве альтернативы, вы можете предоставить список глобальных переменных, которые должны быть исключены из операций резервного копирования и восстановления, например, так:
final class MyTest extends TestCase
{
protected $backupGlobalsExcludeList = ['globalVariable'];
// ...
}
Примечание
Установка свойства $backupGlobalsExcludeList внутри, например, метода setUp(), не имеет эффекта.
Аннотация @backupStaticAttributes, о которой говорится в @backupStaticAttributes, может использоваться для резервного копирования всех значений статических свойств во всех объявленных классах перед каждым тестом и их последующего восстановления.
Обрабатываются все классы, объявленные в момент запуска теста, а не только сам класс теста. Она применяется только к статическим свойствам класса, а не к статическим переменным внутри функций.
Примечание
Операция @backupStaticAttributes выполняется перед методом теста, но только если она включена. Если статическое значение было изменено ранее выполненным тестом, у которого @backupStaticAttributes не было включено, то это значение будет заархивировано и восстановлено, а не изначально объявленное значение по умолчанию. PHP не записывает изначально объявленное значение по умолчанию для какой-либо статической переменной.
То же самое относится к статическим свойствам классов, которые были недавно загружены/объявлены в тесте. Они не могут быть сброшены до их изначально объявленного значения по умолчанию после теста, так как это значение неизвестно. Любое установленное значение просочится в последующие тесты.
Для модульных тестов рекомендуется явно сбрасывать значения статических свойств под тестированием в коде setUp() (и, желательно, также tearDown(), чтобы не влиять на последующие тесты).
Вы можете предоставить список статических атрибутов, которые необходимо исключить из операций резервного копирования и восстановления:
final class MyTest extends TestCase
{
protected $backupStaticAttributesExcludeList = [
'className' => ['attributeName']
];
// ...
}
Примечание
Установка свойства $backupStaticAttributesExcludeList внутри, например, метода setUp(), не имеет эффекта.
© 2005–2020 Sebastian Bergmann
Licensed under the Creative Commons Attribution 3.0 Unported License.
https://phpunit.readthedocs.io/en/9.5/fixtures.html