Компонент PHPUnit Bridge

PHPUnit Bridge предоставляет утилиты для отчётов о тестах наследия и использовании устаревшего кода, а также помощника для тестов, чувствительных ко времени.

Он имеет следующие функции:

  • Заставляет тесты использовать постоянную локаль (C);
  • Авторегистрирует class_exists, чтобы загружать аннотации Doctrine (когда они используются);
  • Отображает полный список устаревших функций, используемых в приложении;
  • Отображает отслеживание стека устаревшей функции по требованю;
  • Предоставляет классы помощника ClockMock и DnsMock для тестов, чувствительных ко времени и сети;
  • Предоставляет изменённую версию PHPUnit, которая не имеет встроенного symfony/yaml или prophecy, чтобы избежать конфликтов с этими зависимостями.

Установка

1
$ composer require --dev "symfony/phpunit-bridge:*"

Также вы можете клонировать репозиторий https://github.com/symfony/phpunit-bridge.

Note

If you install this component outside of a Symfony application, you must require the vendor/autoload.php file in your code to enable the class autoloading mechanism provided by Composer. Read this article for more details.

Note

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

Если вы планируете Write Assertions about Deprecations и использовать обычный скрипт PHPUnit (а не изменённый скрипт PHPUnit, предоставленный Symfony), то вам нужно зарегистрировать новый слушатель теста под названием SymfonyTestsListener:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- http://phpunit.de/manual/6.0/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.0/phpunit.xsd"
>

    <!-- ... -->

    <listeners>
        <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
    </listeners>
</phpunit>

Использование

Эта статья объясняет как использовать функции PhpUnitBridge как независимого компонента в любом приложении PHP. Прочитайте статью Testing для понимания как использовать его в приложениях Symfony.

Когда компонент установлен, создаётся скрипт simple-phpunit в каталоге vendor/ для запуска тестов. Этот скрипт создаёт оболочку для исходного бинарного PHPUnit, чтобы предоставить больше функций:

1
2
$ cd my-project/
$ ./vendor/bin/simple-phpunit

После выполнения ваших тестов PHPUnit, вы получите отчёт, похожий на этот:

../_images/report.png

Этот отчёт включает в себя:

Незаглушенные
Сообщает об уведомлениях об устаревании, которые были запущены без рекомендованного оператора @-silencing.
Унаследованные
Уведомления об устаревании помечают тесты, которые ясно тестируют какие-то функци наследования.
Оставшиеся/Другие
Все другие (не наследственные) уведомления об устаревании, сгруппированные по собщению, классу теста и методу.

Note

Если вы не хотите использовать скрипт simple-phpunit, зарегистрируйте следующий слушатель событий PHPUnit в вашем файле конфигурации PHPUnit, чтобы получить такой же отчёт об устареваниях (который создаётся обработчиком ошибок PHP, под названием DeprecationErrorHandler):

1
2
3
4
5
<!-- phpunit.xml.dist -->
<!-- ... -->
<listeners>
    <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>

Вызов уведомлений об устаревании

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

1
@trigger_error('Your deprecation message', E_USER_DEPRECATED);

Без оператора @-silencing пользователям было бы нужно отказываться от уведомлений об устаревании. Заглушение этого поведения меняет его настройки по умолчанию и позволяет пользователям самим выбирать, когда они готовы с ними справляться (путём добавления пользовательского обработчика ошибок, вроде предоставленного этим мостом). Если они не заглушены, уведомления об устаревании будут появляться в разделе Незаглушенные отчёта об устаревании.

Помечание тестов в качестве наследования

Существует три способа отметить тест, как наследуемый:

  • (Рекомендовано) Добавьте аннотацию @group legacy к его классу или методу;
  • Сделайте так, чтобы имя его класса начиналось с префикса Legacy;
  • Сделайте так, чтобы имя его метода начиналось с testLegacy*() вместо test*().

Note

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

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

Конфигурация

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!-- http://phpunit.de/manual/6.0/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.0/phpunit.xsd"
>

    <!-- ... -->

    <php>
        <server name="KERNEL_CLASS" value="App\Kernel" />
        <env name="SYMFONY_DEPRECATIONS_HELPER" value="/foobar/" />
    </php>
</phpunit>

PHPUnit остановит ваш пакет тестов, как только будет вызвано уведомление об устаревании, сообщение которого содержит строку "foobar".

Как сделать, чтобы тест был неуспешным

По умолчанию, все сообщения об устаревании, тегированные не-наследственными или не-@-silenced заставят тесты терпеть неудачу. Как вариант, вы можете установить SYMFONY_DEPRECATIONS_HELPER в арбитражное значение (например, 320), что сделает ваши тесты неуспешными только, если будет достигнуто большее количество уведомлений об устаревании (0 - значение по умолчанию). Вы можете также установить значение "weak", которое заставит мост игнорировать все уведомления об устаревании. Это полезно для проктов. которые должны использовать устаревшие интерфейсы в связи с обратной совместимостью.

Когда вы содержите библиотеку, неудача пакета тестов при первом появлении устаревания в зависимости, нежелательно, так как это возлагает исправление этого устаревания на любого вкладчика, который отправляет запрос на включение вскоре после того, как с был сделан релиз поставщика с этим устареванием. Чтобы смягчить это, вы можете либо использвать более чёткие требования, надеясь, что зависимости не вызовут новых устареваний в версии патча, либо даже зафиксировать файл блокировки Composer, который будет создавать другой класс проблем. Поэтому библиотеки будут часто использовать SYMFONY_DEPRECATIONS_HELPER=weak. Это имеет недостаток в виде внесения вкладчиками своих устареваний, но:

  • забывает исправлять устаревшие вызовы, если они есть;
  • забывает отмечать соответствующие тесты аннотациями @group legacy.

При использовании значения "weak_vendors", устаревания, вызванные вне каталога vendors будут делать пакет тестов неуспешным, а устаревания, вызыванные в библиотеке внутри - не будут, что даст вам максимум преимуществ.

Отключение помощника устареваний

Установите переменную окружения SYMFONY_DEPRECATIONS_HELPER, как disabled, чтобы полностью отключить помощника устареваний. Это полезно для использования остальных функций, предоставляемых этим компонентом, не получая ошибок или сообщений, относящихся к устареваниям.

Напишите утверждения об устареваниях

При добавлении устареваний в ваш код, вам может захотеться писать тесты, проверяющие, чтобы они вызывались, как требуется. Чтобы сделать это, мост предоставляет аннотацию @expectedDeprecation, которую вы можете использовать в ваших методах тестов. Она требует, чтобы вы передали ожидаемое сообщение, данное в том же формате, что и для метода PHPUnit assertStringMatchesFormat(). Если вы ожидаете более одного сообщения об устаревании для заданного метода теста, вы можете использовать аннотацию несколько раз (порядок имеет значение):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
 * @group legacy
 * @expectedDeprecation This "%s" method is deprecated.
 * @expectedDeprecation The second argument of the "%s" method is deprecated.
 */
public function testDeprecatedCode()
{
    @trigger_error('This "Foo" method is deprecated.', E_USER_DEPRECATED);
    @trigger_error('The second argument of the "Bar" method is deprecated.', E_USER_DEPRECATED);
}

Тесты, чувствительные ко времени

Случаи применения

Если у вас есть такие тесты, чувствительные ко времени:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use PHPUnit\Framework\TestCase;
use Symfony\Component\Stopwatch\Stopwatch;

class MyTest extends TestCase
{
    public function testSomething()
    {
        $stopwatch = new Stopwatch();

        $stopwatch->start('event_name');
        sleep(10);
        $duration = $stopwatch->stop('event_name')->getDuration();

        $this->assertEquals(10, $duration);
    }
}

Вы использвали Компонент Symfony Секундомер, чтобы подсчитать длительность вашего процесс, тут - 10 секунд. Однако, в зависимости от нагрузки на сервер или процессов, запущенных на вашей локальой машине, $duration может, к примеру, быть 10.000023s вместо 10s.

Такие тесты называются переходными тестами: они терпят неудачи рандомно, в зависимости от побочных и внешних обстоятельств. Они часто вызывают проблемы при использовании постоянных публичных сервисов интеграции вроде Travis CI.

Имитация часов

Класс ClockMock, предоставленный этим мостом, позволяет вам имитировать встроенные PHP функции time(), microtime(), sleep() и usleep().

Чтобы использовать в вашем тесте класс ClockMock, добавьте аннотацию @group time-sensitive к его классу или методам. Эта аннотация работает только при выполнении PHPUnit, используя скрипт vendor/bin/simple-phpunit, или при регистрации следующего слушателя в вашей конфигурации PHPUnit:

1
2
3
4
5
<!-- phpunit.xml.dist -->
<!-- ... -->
<listeners>
    <listener class="\Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>

Note

Если вы не хотите использовать аннотацию @group time-sensitive, вы можете зарегистрировать класс ClockMock вручную, вызвав ClockMock::register(__CLASS__) и ClockMock::withClockMock(true) до теста, а ClockMock::withClockMock(false) - после.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use PHPUnit\Framework\TestCase;
use Symfony\Component\Stopwatch\Stopwatch;

/**
 * @group time-sensitive
 */
class MyTest extends TestCase
{
    public function testSomething()
    {
        $stopwatch = new Stopwatch('event_name');

        $stopwatch->start();
        sleep(10);
        $duration = $stopwatch->stop('event_name')->getDuration();

        $this->assertEquals(10000, $duration);
    }
}

И это всё!

Tip

Дополнительный бонус использования класса ClockMock - время проходит моментально. Использование PHP sleep(10) заставит ваш тест ждать 10 настоящих секунд (плюс минус). В противовес этому, класс ClockMock переводит внуренние часы на заданное количество секунд, не ожидая это время на самом деле, поэтому ваш тест будет выполнен на 10 секунд быстрее.

Тесты, чувствительные к СДИ

Тесты, создающие соединения сети, например, проверка валидности записи СДИ (Системы доменных имён) может быть долгой в выполнении и ненадёжной вследствие условий сети. По этой причине, данный компонент также предоставляет имитации этих PHP функций:

Случаи применения

Рассмотрите следующий пример, который использует опцию checkMX ограничения Email, чтобы протестировать валидность домена электронной почты:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints\Email;

class MyTest extends TestCase
{
    public function testEmail()
    {
        $validator = ...
        $constraint = new Email(array('checkMX' => true));

        $result = $validator->validate('[email protected]', $constraint);

        // ...
}

Чтобы избежать создания настоящего соединения сети, добавьте аннотацию @dns-sensitive к классу и используйте DnsMock::withMockedHosts(), чтобы сконфигурировать данные, которые вы ожидаете получить для заданных хостов:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints\Email;

/**
 * @group dns-sensitive
 */
class MyTest extends TestCase
{
    public function testEmails()
    {
        DnsMock::withMockedHosts(array('example.com' => array(array('type' => 'MX'))));

        $validator = ...
        $constraint = new Email(array('checkMX' => true));

        $result = $validator->validate('[email protected]', $constraint);

        // ...
}

Конфигурация метода withMockedHosts() определяется в виде массива. Ключами являются сымитированные хосты, а значениями - массивы записей СДИ в том же формате, что возвращается dns_get_record, чтобы вы могли симулировать разнообразные условия сети:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
DnsMock::withMockedHosts(array(
    'example.com' => array(
        array(
            'type' => 'A',
            'ip' => '1.2.3.4',
        ),
        array(
            'type' => 'AAAA',
            'ipv6' => '::12',
        ),
    ),
));

Диагностика и устранение неполадок

Аннотации @group time-sensitive и @group dns-sensitive работают "по соглашению" и предполагают, что пространства имён тестируемого класса могут быть получены просто путём удаления части Tests\ из пространств имён тестов. Т.е., если полное имя класса вашего случая тестирования - App\Tests\Watch\DummyWatchTest, он предполагает, что пространство имён тестируемого класса - App\Watch.

Если это соглашение не работает для вашего приложение, сконфигурируйте имитацию пространств имён в файле phpunit.xml, как это делается, например, в HttpKernel Component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!-- http://phpunit.de/manual/4.1/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
>

    <!-- ... -->

    <listeners>
        <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
            <arguments>
                <array>
                    <element key="time-sensitive"><string>Symfony\Component\HttpFoundation</string></element>
                </array>
            </arguments>
        </listener>
    </listeners>
</phpunit>

Изменённый скрипт PHPUnit

Этот мост предоставляет изменённую версию PHPUnit, которую вы можете вызвать, используя его команду bin/simple-phpunit. Она имеет следующие функции:

  • Не встраивает symfony/yaml или prophecy, чтобы избежать конфликтов с этими зависимостями;
  • Использует PHPUnit 4.8 при запуске с PHP <=5.5,и PHPUnit 5.3 при запуске с PHP >=5.6;
  • Собирает и повторяет пропущенные тесты, когда определена переменная окружения SYMFONY_PHPUNIT_SKIPPED_TESTS: она должна указывать имя файла, который будет использован для хранения пропущенных тестов при первом запуске, и повторять их при повторном запуске;
  • Параллелит выполнение пакета тестов, когда каталог задан в качестве аргумента, сканируя этот каталог на предмет файлов phpunit.xml.dist до уровня SYMFONY_PHPUNIT_MAX_DEPTH (указан как переменная окружения, по умолчанию - 3);

Скрипт пишет изменённый PHPUnit, который он строит, в каталоге, который можно сконфигурировать с помощью SYMFONY_PHPUNIT_DIR, или в том же каталоге, что и simple-phpunit, если он не предоставлен.

Также можно установить эту переменную окружения в файле phpunit.xml.dist.

Если вы установили мост через Composer, то вы можете запустить его, вызвав, к примеру:

1
$ vendor/bin/simple-phpunit

Tip

Установите переменную окружения SYMFONY_PHPUNIT_VERSION например, 5.5, чтобы изменить базовую версию PHPUnit на 5.5 вместо значения по умолчанию 5.3.

Также можно установить эту переменную окружения в файле phpunit.xml.dist.

Tip

Если вам всё ещё надо использовать prophecy (но не symfony/yaml), то установите переменную окружения SYMFONY_PHPUNIT_REMOVE, как symfony/yaml.

Также можно установить эту переменную окружения в файле phpunit.xml.dist.

Слушатель покрытия кода

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

Рассмотрите следующий пример:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Bar
{
    public function barMethod()
    {
        return 'bar';
    }
}

class Foo
{
    private $bar;

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

    public function fooMethod()
    {
        $this->bar->barMethod();

        return 'bar';
    }
}

class FooTest extends PHPUnit\Framework\TestCase
{
    public function test()
    {
        $bar = new Bar();
        $foo = new Foo($bar);

        $this->assertSame('bar', $foo->fooMethod());
    }
}

Метод FooTest::test выполняет каждую строчку кода классов Foo и Bar, но Bar на самом деле не тестируется. CoverageListener направлен на исправление этого поведения, путём добавления соответствующей аннотации @covers в каждом тесте класса.

Если класс теста уже определяет аннотацию @covers, то этот слушатель ничего не делает. В обратном случае, он пытается найти код, связанный с тестом, удалив часть имени класса Test: My\Namespace\Tests\FooTest -> My\Namespace\Foo.

Установка

Добавьте следующую конфигурацию к файлу phpunit.xml.dist

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- http://phpunit.de/manual/6.0/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.0/phpunit.xsd"
>

    <!-- ... -->

    <listeners>
        <listener class="Symfony\Bridge\PhpUnit\CoverageListener" />
    </listeners>
</phpunit>

Если логика, используемая для поиска соответствующего кода, слишком простая или не рабтает для вашего приложения, вы можете использовать собственный разрешитель SUT (Испытываемой системы):

1
2
3
4
5
6
7
<listeners>
    <listener class="Symfony\Bridge\PhpUnit\CoverageListener">
        <arguments>
            <string>My\Namespace\SutSolver::solve</string>
        </arguments>
    </listener>
</listeners>

My\Namespace\SutSolver::solve может быть любым PHP вызываемым, которое получает текущее имя класса теста и его первый аргумент.

Наконец, слушатель также может отобразить предупреждающие сообщения, если разрешитель SUT не находит SUT:

1
2
3
4
5
6
7
8
<listeners>
    <listener class="Symfony\Bridge\PhpUnit\CoverageListener">
        <arguments>
            <null/>
            <boolean>true</boolean>
        </arguments>
    </listener>
</listeners>

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.