Компонент PHPUnit Bridge

Дата обновления перевода: 2024-07-10

Компонент PHPUnit Bridge

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

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

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

    1. разделять зависимости вашего приложения и phpunit, чтобы предотвратить применение нежелаемых ограничений;
    2. запускать тесты параллельно, когда набор тестов разделен на несколько файлов phpunit.xml;
    3. записывать и повторно запускать пропущенные тесты;
  • Позволяет создавать тести, совместимые со множеством версий PHPUnit (так как предоставляет полизаполнения для недостающих методов, псевдонимы пространства имен для классов без пространств имен и т.д.)

Установка

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

Note

Если вы устанавливаете этот компонент вне приложения Symfony, вам нужно подключить файл vendor/autoload.php в вашем коде для включения механизма автозагрузки классов, предоставляемых Composer. Детальнее читайте в этой статье.

Note

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

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

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

    <!-- ... -->

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

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

See also

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ ./vendor/bin/simple-phpunit
  PHPUnit by Sebastian Bergmann.

  Конфигурация, считанная из <your-project>/phpunit.xml.dist
  .................

  Time: 1.77 seconds, Memory: 5.75Mb

  OK (17 тестов, 21 утверждений)

  Оставшиеся уведомления об устареваниях (2)

  getEntityManager устарел в Symfony 2.1. Вместо этого используйте getManager: 2x
    1x в DefaultControllerTest::testPublicUrls из App\Tests\Controller
    1x в BlogControllerTest::testIndex из App\Tests\Controller

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

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

Note

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

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

Параллельный запуск тестов

Измененный скрипт PHPUnit позволяет запускать тесты параллельно, предоставляя каталог, содержащий множество наборов тестов с собственным phpunit.xml.dist.

1
2
3
4
5
6
7
├── tests/
│   ├── Functional/
│   │   ├── ...
│   │   └── phpunit.xml.dist
│   ├── Unit/
│   │   ├── ...
│   │   └── phpunit.xml.dist
1
$ ./vendor/bin/simple-phpunit tests/

Изменённый скрипт PHPUnit будет рекурсивно продвигаться по предоставленному каталогу, на глубину до 3 суб-каталогов или значения, указанного переменной окружения SYMFONY_PHPUNIT_MAX_DEPTH, в поисках файлов phpunit.xml.dist, а затем запускать каждый найденный набор параллельно, собирая их вывод и отображая результаты каждого набора тестов в собственном разделе.

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

Уведомления об устаревании могут быть вызваны используяtrigger_deprecation из пакета symfony/deprecation-contracts:

1
2
3
4
5
// обозначает, что что-то устарело с версии 1.3 vendor-name/packagename
trigger_deprecation('vendor-name/package-name', '1.3', 'Your deprecation message');

// вы также можете использовать формат printf (все аргументы после сообщения будут использованы)
trigger_deprecation('...', '1.3', 'Value "%s" is deprecated, use ...  instead.', $value);

Обозначение тестов как наследуемых

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

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

Note

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

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

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

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

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

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

По умолчанию, все не тегированные наследумым или не заглушенные (@-silencing operator) сообщения об устаревании, заставят тесты терпеть неудачу. Как вариант, вы можете сконфигурировать произвольный пороговый уровень, установив SYMFONY_DEPRECATIONS_HELPER как max[total]=320, к примеру. Это сделает ваши тесты неуспешными только, если будет достигнуто большее количество уведомлений об устаревании (0 - значение по умолчанию).

Вы можете иметь более детальный контроль, используя другие ключи массива max, то есть self, direct, и indirect. Переменная окружения SYMFONY_DEPRECATIONS_HELPER принимает строку, зашифрованную URL, что означает, что вы можете сочетать пороговые уровни и любую другую настройку конфигурации, вроде этого: SYMFONY_DEPRECATIONS_HELPER='max[total]=42&max[self]=0&verbose=0'

Внутренние устаревания

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

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

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

При использовании значения SYMFONY_DEPRECATIONS_HELPER=max[self]=0, устаревания, вызванные вне каталога vendors будут рассматриваться отдельно, в то время как устаревания, запущенные изнутри библиотеки - нет (разве что вы достигнете количества 999999), что даст вам максимум преимуществ.

Прямые и непрямые устаревания

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

Конфигурация max[direct] позволяет вам устанавливать пороговое значение только для прямых устареваний, чтобы вы могли заметить, когда ваш код использует устаревшие API, и не отставать от изменений. Вы можете продолжать использовать max[indirect], если вы хотите держать непрямые устарвания в рамках заданных пороговых значений.

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

???????? ??????????????? ????????
max[total]=0 ????????????? ??? ??????? ?????????????? ???????? ? ????????/?????????????? ?????????????.
max[direct]=0 ????????????? ??? ???????? ? ?????????????, ??????? ?? ???????? ?? ?????? ?????????????.
max[self]=0 ????????????? ??? ?????????, ????? ???????????? ??????? ???????????, ? ??????? ?? ????? ????????? ???? ???????????? ???? ?? ??????? ????.

Игнорирование устареваний

Если ваше приложение имеет какие-то устаревания, которые вы по каким-либо причинам не можете исправить, вы можете указать Symfony игнорировать их.

Сначала вам нужно создать текстовый файл, где каждая строчка - устаревание, которое нужно игнорировать, определённое в качестве регулярного выражения. Строчки, начинающиеся с хеша (#), считаются комментариями:

1
2
3
4
5
# Этот файл содержит паттерны, которые нужно игнорировать во время тестирования использования
# устаревшего кода.

%The "Symfony\\Component\\Validator\\Context\\ExecutionContextInterface::.*\(\)" method is considered internal Used by the validator engine\. (Should not be called by user\W+code\. )?It may change without further notice\. You should not extend it from "[^"]+"\.%
%The "PHPUnit\\Framework\\TestCase::addWarning\(\)" method is considered internal%

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

1
$ SYMFONY_DEPRECATIONS_HELPER='ignoreFile=./tests/baseline-ignore' ./vendor/bin/simple-phpunit

Базовые устаревания

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

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

1
$ SYMFONY_DEPRECATIONS_HELPER='generateBaseline=true&baselineFile=./tests/allowed.json' ./vendor/bin/simple-phpunit

Эта команда сохраняет все заявленные устаревания при прогоне тестов по заданному пути файла и зашифровывается в JSON.

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

1
$ SYMFONY_DEPRECATIONS_HELPER='baselineFile=./tests/allowed.json' ./vendor/bin/simple-phpunit

Отключение словесного вывода

По умолчанию, мост отобразит детализированный вывод с количеством устареваний и мест, где они встречаются. Если этого для вас слишком много, вы можете использовать SYMFONY_DEPRECATIONS_HELPER=verbose=0, чтобы отключить словесный вывод.

Также возможно изменить словесность для каждого типа устаревания. Например, использование quiet[]=indirect&quiet[]=other спрячет детали для устареваний типа "indirect" и "other".

Опция quiet скрывает подробности для указанных типов устаревания, но не изменяет результат с точки зрения кода выхода. Именно для этого предназначен параметр max и обе настройки ортогональны.

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

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

Уведомления об устаревании во время автозагрузки

По умолчанию, PHPUnit Bridge использует DebugClassLoader из компонента ErrorHandler, чтобы вызвать уведомления устаревания во время автозагрузки класса. Это можно отключить с опцией debug-class-loader.

1
2
3
4
5
6
7
8
9
10
11
12
<!-- phpunit.xml.dist -->
<!-- ... -->
<listeners>
    <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
        <arguments>
            <array>
                <!-- установите эту опцию как 0, чтобы отключить интеграцию DebugClassLoader -->
                <element key="debug-class-loader"><integer>0</integer></element>
            </array>
        </arguments>
    </listener>
</listeners>

Устаревания во время компиляции

Используйте команду debug:container, чтобы перечислить устревания, сгенерированные во время компиляции и разогрева контейнера:

1
$ php bin/console debug:container --deprecations

Устаревания логов

Для отключения словесного вывода и его записи в файл логов, вы можете использовать SYMFONY_DEPRECATIONS_HELPER='logFile=/path/deprecations.log'.

Установка локали для тестов

По умолчанию PHPUnit Bridge устанавливает локаль как C, чтобы избежать проблем с локалью в тестах.Это поведение можно изменить, установив переменную окружения SYMFONY_PHPUNIT_LOCALE как нужную локаль:

1
2
# .env.test
SYMFONY_PHPUNIT_LOCALE="fr_FR"

Кроме того, вы можете установить эту переменную окружения в файле конфигурации PHPUnit :

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

    <!-- ... -->

    <php>
        <!-- ... -->
        <env name="SYMFONY_PHPUNIT_LOCALE" value="fr_FR"/>
    </php>
</phpunit>

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;

class MyTest extends TestCase
{
    use ExpectDeprecationTrait;

    /**
     * @group legacy
     */
    public function testDeprecatedCode(): void
    {
        // протестировать код, вызывающий следующее устаревание:
        // trigger_deprecation('vendor-name/package-name', '5.1', 'This "Foo" method is deprecated.');
        $this->expectDeprecation('Since vendor-name/package-name 5.1: This "%s" method is deprecated');

        // ...

        // протестировать код, вызывающий следующее устаревание:
        // trigger_deprecation('vendor-name/package-name', '4.4', 'The second argument of the "Bar" method is deprecated.');
        $this->expectDeprecation('Since vendor-name/package-name 4.4: The second argument of the "%s" method is deprecated.');
    }
}

Отображение полной трассировки стека

По умолчанию, PHPUnit Bridge отображает только сообщения об устаревании. Чтобы отобразить полную трассировку стека, связанную с устареванием, установите значение SYMFONY_DEPRECATIONS_HELPER как регулярное выражение, совпадающее с сообщением об устаревании.

Например, если вызывается следующее уведомление об устаревании:

1
2
1x: Doctrine\Common\ClassLoader is deprecated.
  1x in EntityTypeTest::setUp from Symfony\Bridge\Doctrine\Tests\Form\Type

Запуск следующей команды отобразить полную трассировку стегка:

1
$ SYMFONY_DEPRECATIONS_HELPER='/Doctrine\\Common\\ClassLoader is deprecated\./' ./vendor/bin/simple-phpunit

Тестирование с несколькими версиями PHPUnit

При тестировании библиотеки, которая должна быть совместимой с несколькими версиями PHP, пакет тестов не может использовать самые новые версии PHPUnit, потому что:

  • В PHPUnit 8 устарели несколько методов, уступив место другим методам, которые не доступны в более старых версиях (например, PHPUnit 4);
  • В PHPUnit 8 был добавлен возвратный тип void к методу setUp(), что не совместимо с PHP 5.5;
  • PHPUnit перешел на классы пространств имен, начиная с PHPUnit 6, поэтому тесты должны работать и с, и без пространств имен.

Полизаполнения для недоступных методов

При использовании скрипта simple-phpunit, PHPUnit Bridge внедряет полизаполнения для большинства методов классов TestCase и Assert (например, expectException(), expectExceptionMessage(), assertContainsEquals(), и т.д.). Это позвляет писать случае тестирования, используя последние лучшие практики, но оставаться совместимыми с более старыми версиями PHPUnit.

Удаление возвратного типа Void

При выполнении скрипта simple-phpunit с переменной окружения SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT, установленной, как 1, PHPUnit bridge изменит код PHPUnit, чтобы удалить возвратный тип (представлено в PHPUnit 8) из методов setUp(), tearDown(), setUpBeforeClass() и tearDownAfterClass(). Это позволяет вам писать тесты, совместимые как с PHP 5, так и с PHPUnit 8.

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

PHPUnit bridge добавляет псевдонимы с пространством имен для большинства классов PHPUnit, заявленных без пространства имен (например, PHPUnit_Framework_Assert), что позволяет вам всегда использовать заявление класса с пространством имен, даже если тест выполняется с помощью PHPUnit 4.

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

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

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

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(): void
    {
        $stopwatch = new Stopwatch();

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

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

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

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

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

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

Другие функции с необязательным параметром временной отметки, который по умолчанию имеет значение time(), будут продолжать использовать системное время вместо сымитированного. Это означает, что вам может понадобиться изменить какую-то часть кода в ваших тестах. Например, вместо new DateTime(), вам нужно использовать DateTime::createFromFormat('U', time()), чтобы использовать сымитированную функцию time().

Чтобы использовать в вашем тесте класс 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(): void
    {
        $stopwatch = new Stopwatch();

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

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

И это всё!

Caution

Имитация функции, основанная на времени, следует правилам разрешения пространства имен PHP, поэтому "полностью квалифицированные функциональные вызовы" (например, \time()) имитировать нельзя.

Аннотация @group time-sensitive эквивалентна вызову ClockMock::register(MyTest::class). Если вы хотите сымитировать функцию, используемую в другом классе, сделайте это ясно, используя ClockMock::register(MyClass::class):

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
// класс, использующий функцию time(), будет сымитирован
namespace App;

class MyClass
{
    public function getTimeInHours(): void
    {
        return time() / 3600;
    }
}

// тест, ясно имитирующий внешнюю функцию time()
namespace App\Tests;

use App\MyClass;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ClockMock;

/**
 * @group time-sensitive
 */
class MyTest extends TestCase
{
    public function getTimeInHours(): void
    {
        ClockMock::register(MyClass::class);

        $my = new MyClass();
        $result = $my->getTimeInHours();

        $this->assertEquals(time() / 3600, $result);
    }
}

Tip

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
use App\Validator\DomainValidator;
use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    public function testEmail(): void
    {
        $validator = new DomainValidator(['checkDnsRecord' => true]);
        $isValid = $validator->validate('example.com');

        // ...
    }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use App\Validator\DomainValidator;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\DnsMock;

/**
 * @group dns-sensitive
 */
class DomainValidatorTest extends TestCase
{
    public function testEmails(): void
    {
        DnsMock::withMockedHosts([
            'example.com' => [['type' => 'A', 'ip' => '1.2.3.4']],
        ]);

        $validator = new DomainValidator(['checkDnsRecord' => true]);
        $isValid = $validator->validate('example.com');

        // ...
    }
}

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

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

Тесты, основанные на существовании классов

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

Случай использования

Рассмотрите следующий пример, полагающийся на Vendor\DependencyClass, чтобы изменять поведение:

1
2
3
4
5
6
7
8
9
10
11
12
13
use Vendor\DependencyClass;

class MyClass
{
    public function hello(): string
    {
        if (class_exists(DependencyClass::class)) {
            return 'The dependency behavior.';
        }

        return 'The default behavior.';
    }
}

Обычный пример теста для MyClass (предполагая, что зависимости разработки устанавливаются во время тестов) будет выглядеть так:

1
2
3
4
5
6
7
8
9
10
11
12
13
use MyClass;
use PHPUnit\Framework\TestCase;

class MyClassTest extends TestCase
{
    public function testHello(): void
    {
        $class = new MyClass();
        $result = $class->hello(); // "The dependency behavior."

        // ...
    }
}

Для того, чтобы протестировать поведение по умолчанию, используйте ClassExistsMock::withMockedClasses(), чтобы сконфигурировать ожидаемые классы, интерфейсы и/или черты для запуска кода:

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

class MyClassTest extends TestCase
{
    // ...

    public function testHelloDefault()
    {
        ClassExistsMock::register(MyClass::class);
        ClassExistsMock::withMockedClasses([DependencyClass::class => false]);

        $class = new MyClass();
        $result = $class->hello(); // "The default behavior."

        // ...
    }
}

Отметьте, что имитация класса с ClassExistsMock::withMockedClasses() заставит class_exists, interface_exists и trait_exists возвращать true.

Чтобы зарегистрировать исчисление и сымитировать enum_exists, должен быть использован ClassExistsMock::withMockedEnums(). Отметьте, что как в PHP 8.1 и позднее, вызов class_exists в исчислении вернёт true. Поэтому вызов ClassExistsMock::withMockedEnums() также зарегистрирует исчисление как сымитированный класс.

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

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

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

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

или * Зарегистрировать пространства имен в конце файла config/bootstrap.php.

1
2
3
4
5
6
7
8
9
10
11
<!-- phpunit.xml.dist -->
<!-- ... -->
<listeners>
    <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
            <arguments>
                <array>
                    <element key="time-sensitive"><string>Acme\MyClassTest</string></element>
                </array>
            </arguments>
        </listener>
</listeners>
1
2
3
4
5
6
7
// config/bootstrap.php
use Symfony\Bridge\PhpUnit\ClockMock;

// ...
if ('test' === $_SERVER['APP_ENV']) {
    ClockMock::register('Acme\\MyClassTest\\');
}

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

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

  • Работает с отдельным каталогом поставщиков, который не конфликтует с вашими;
  • Не встраивает prophecy, чтобы избежать конфликтов с этими зависимостями;
  • Собирает и повторяет пропущенные тесты, когда определена переменная окружения 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

Возможно изменить базовую версию PHPUnit, установив переменную окружения SYMFONY_PHPUNIT_VERSION в файле phpunit.xml.dist (например, <server name="SYMFONY_PHPUNIT_VERSION" value="5.5"/>). Это предпочитаемый метод, так как его можно отправить в ваше хранилище контроля версий.

Также возможно установить SYMFONY_PHPUNIT_VERSION как реальную переменную окружения (не определенную в файле dotenv ).

Таким же образом, SYMFONY_MAX_PHPUNIT_VERSION установит максимальную рассматриваемую версию PHPUnit. Это полезно при тестировании фреймворка, который не поддерживает самую(ые) новую(ые) версию(и) PHPUnit.

Tip

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

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

Tip

Также возможно требовать дополнительные пакеты, которые будут установлены вместе с остальными необходимыми пакетами PHPUnit, используя переменную окружения SYMFONY_PHPUNIT_REQUIRE. Это особенно полезно для установки плагинов PHPUnit, без необходимости добавлять их в ваш основной файл composer.json. Необходимые пакеты должны быть разделены пробелом.

1
2
3
4
5
<!-- phpunit.xml.dist -->
<!-- ... -->
<php>
    <env name="SYMFONY_PHPUNIT_REQUIRE" value="vendor/name:^1.2 vendor/name2:^3"/>
</php>

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

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

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

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
class Bar
{
    public function barMethod(): string
    {
        return 'bar';
    }
}

class Foo
{
    public function __construct(
        private Bar $bar,
    ) {
    }

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

        return 'bar';
    }
}

class FooTest extends PHPUnit\Framework\TestCase
{
    public function test(): void
    {
        $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
<!-- https://phpunit.de/manual/6.0/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://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>