Компонент PHPUnit Bridge

Дата оновлення перекладу 2024-05-03

Компонент 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 tests, 21 assertions)

  Сповіщення про застарівання, що залишилися (2)

  getEntityManager застарів з Symfony 2.1. Використайте getManager натомість: 2x
    1x in DefaultControllerTest::testPublicUrls from App\Tests\Controller
    1x in BlogControllerTest::testIndex from 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"

Як варіант, ви можете встановити цю змінну середовища в файлі конфігурації PHPUit:

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', 'Цей метод "Foo" застарів.');
        $this->expectDeprecation('Since vendor-name/package-name 5.1: Цей метод "%s" застарів');

        // ...

        // протестувати код, що викликає наступне застарівання:
        // trigger_deprecation('vendor-name/package-name', '4.4', 'Другий аргумент методу "Bar" застарів.');
        $this->expectDeprecation('Since vendor-name/package-name 4.4: Другий аргумент методу "%s" застарів.');
    }
}

Відображення повного трасування стеку

За замовчуванням, 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', (string) 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(); // "Поведінка залежності."

        // ...
    }
}

Для того, щоб протестувати поведінку за замовчуванням, використайте 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>