Spec-Zone.ru › PHPUnit

Покрытие кода

Википедия:

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

В этой главе вы узнаете всё о функциональности PHPUnit для анализа покрытия кода, которая позволяет понять, какие части рабочего кода выполняются при запуске тестов. Для этого используется библиотека php-code-coverage, которая, в свою очередь, задействует функциональность анализа покрытия кода, предоставляемую расширениями PHP PCOV или Xdebug.

Примечание

Если при запуске тестов вы видите предупреждение о том, что драйвер покрытия кода недоступен, это означает, что вы используете двоичный файл PHP CLI (php) и не загрузили PCOV или Xdebug.

Примечание

Чтобы использовать Xdebug для сбора данных о покрытии кода, необходимо активировать режим coverage Xdebug.

Предупреждение

Покрытие ветвей выражения match ненадёжно. Всё выражение match может быть отмечено как покрытое, даже если набор тестов выполнил не все его ветви. Это известное ограничение связано с тем, как драйверы покрытия кода PHP (PCOV, Xdebug) сообщают о покрытии строк для выражений match. Точность сообщаемого покрытия зависит от поведения автозагрузки во время выполнения и порядка запуска тестов.

PHPUnit может создавать отчёт о покрытии кода в формате HTML, а также файлы журналов на основе XML с информацией о покрытии кода в различных форматах (Clover, Cobertura, Crap4J, PHPUnit). Информацию о покрытии кода также можно выводить в текстовом виде (в STDOUT) и экспортировать в виде кода PHP для дальнейшей обработки.

Список параметров командной строки, управляющих функциональностью анализа покрытия кода, см. в разделе Средство запуска тестов из командной строки. Соответствующие параметры конфигурации для формирования отчётов о покрытии кода см. в разделах Элемент <source> и Элемент <coverage>.

Метрики покрытия кода

Для измерения покрытия кода используются различные метрики:

Покрытие строк

Метрика покрытия строк показывает, была ли выполнена каждая исполняемая строка.

Покрытие ветвей

Метрика покрытия ветвей показывает, принимало ли булево выражение каждой управляющей конструкции значения true и false во время выполнения набора тестов.

Покрытие путей

Метрика покрытия путей показывает, был ли пройден каждый возможный путь выполнения функции или метода во время выполнения набора тестов. Путь выполнения — это уникальная последовательность ветвей от входа в функцию или метод до выхода из неё.

Покрытие функций и методов

Метрика покрытия функций и методов показывает, была ли вызвана каждая функция или метод. php-code-coverage считает функцию или метод покрытыми, только если покрыты все их исполняемые строки.

Покрытие классов и трейтов

Метрика покрытия классов и трейтов показывает, покрыт ли каждый метод класса или трейта. php-code-coverage считает класс или трейт покрытым, только если покрыты все его методы.

Индекс антипаттернов риска изменений (CRAP)

Индекс антипаттернов риска изменений (CRAP) рассчитывается на основе цикломатической сложности и покрытия кода для отдельного фрагмента кода. Код с невысокой сложностью и достаточным покрытием тестами будет иметь низкий индекс CRAP. Индекс CRAP можно снизить, написав тесты и выполнив рефакторинг кода для уменьшения его сложности.

Библиотека, используемая PHPUnit, поддерживает все перечисленные выше метрики покрытия кода. Для формирования отчётов о покрытии ветвей и путей данные о покрытии кода необходимо собирать с помощью Xdebug, поскольку PCOV поддерживает только покрытие строк.

Включение файлов

Необходимо настроить, какие файлы исходного кода вы считаете своими и поэтому хотите включить в отчёт о покрытии кода. Поскольку другим функциям PHPUnit также нужно знать, какие файлы исходного кода вы считаете своими, рекомендуется настроить это в файле конфигурации XML (см. раздел Элемент <include>). Также можно воспользоваться параметром командной строки --coverage-filter командной строки.

Параметр конфигурации includeUncoveredFiles позволяет настроить использование фильтра:

  • includeUncoveredFiles="true" (по умолчанию) означает, что в отчёт о покрытии кода включаются все файлы, даже если не была выполнена ни одна строка кода в таком файле

  • includeUncoveredFiles="false" означает, что в отчёт о покрытии кода включаются только файлы, в которых выполнена хотя бы одна строка кода

Чтобы получить полный и достоверный отчёт о покрытии кода, настоятельно рекомендуется использовать настройку по умолчанию.

Выбор фрагментов кода

В тестовом коде можно использовать атрибуты PHPUnit\Framework\Attributes\CoversClass, PHPUnit\Framework\Attributes\CoversMethod и PHPUnit\Framework\Attributes\CoversFunction, чтобы указать фрагменты кода, покрытие которых предполагает проверить тестовый класс.

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

Пример 7.1 демонстрирует это.

Пример 7.1 Тестовый класс, указывающий класс, покрытие которого он хочет проверить
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(Invoice::class)]
#[UsesClass(Money::class)]
final class InvoiceTest extends TestCase
{
    public function testAmountInitiallyIsEmpty(): void
    {
        $this->assertEquals(new Money, (new Invoice)->amount());
    }
}

Атрибуты PHPUnit\Framework\Attributes\UsesClass, PHPUnit\Framework\Attributes\UsesMethod и PHPUnit\Framework\Attributes\UsesFunction можно использовать, чтобы указать фрагменты кода, которые следует исключить из анализа покрытия кода, но которые разрешено использовать покрываемому коду. Это объясняется в разделе о непреднамеренно покрытом коде.

В примере выше атрибут #[CoversClass(Invoice::class)] сообщает PHPUnit, что тесты этого класса тестового случая должны проверить покрытие кода класса Invoice. При запуске тестов этого класса будет обработана только информация о покрытии класса Invoice, а информация о покрытии всего остального кода, который также может выполняться во время этих тестов, будет проигнорирована.

В примере выше атрибут #[UsesClass(Money::class)] сообщает PHPUnit, что во время выполнения тестов этого класса тестового случая ожидается и допускается запуск кода класса Money. Это важно при определении того, следует ли считать тест рискованным, если он выполняет код, запуск которого не предполагался.

Поскольку технически невозможно тестировать подкласс отдельно от его базового класса или классов, атрибуты #[CoversClass] и #[UsesClass] учитывают указанный класс, а также все его родительские классы, если они есть.

Атрибут PHPUnit\Framework\Attributes\CoversNothing можно использовать, чтобы указать, что тесты вообще не должны влиять на покрытие кода. Это может быть полезно при написании интеграционных тестов и позволяет убедиться, что покрытие кода формируется только небольшими тестами.

Пример 7.2 Тест, который не должен влиять на покрытие кода
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\TestCase;

#[CoversNothing]
final class IntegrationTest extends TestCase
{
    public function testRegisteredUserCanLogIn(): void
    {
        // ...
    }
}

Исключение блоков кода

Иногда встречаются фрагменты кода или даже отдельные строки кода, которые невозможно протестировать и которые вы хотите исключить из анализа покрытия кода. PHPUnit позволяет сделать это с помощью аннотаций @codeCoverageIgnore, @codeCoverageIgnoreStart и @codeCoverageIgnoreEnd, которые можно добавлять в комментарии кода рабочего окружения:

Пример 7.3 Использование аннотаций @codeCoverageIgnore, @codeCoverageIgnoreStart и @codeCoverageIgnoreEnd
<?php declare(strict_types=1);
/**
 * @codeCoverageIgnore
 */
final class Foo
{
    public function bar(): void
    {
    }
}

final class Bar
{
    /**
     * @codeCoverageIgnore
     */
    public function foo(): void
    {
    }
}

if (false) {
    // @codeCoverageIgnoreStart
    print '*';
    // @codeCoverageIgnoreEnd
}

exit; // @codeCoverageIgnore

В приведённом выше примере аннотация @codeCoverageIgnore используется для исключения всего кода класса Foo, всего кода метода Bar::foo() и отдельной строки кода с оператором exit;. Строка с оператором print '*'; исключается с помощью // @codeCoverageIgnoreStart и // @codeCoverageIgnoreEnd.

PHPCOV

PHPCOV — это инструмент командной строки для работы с сериализованными данными покрытия кода (файлами *.cov), созданными PHPUnit.

PHPCOV предоставляет две команды: merge для объединения данных о покрытии кода из нескольких запусков тестов и patch-coverage для анализа покрытия изменённых строк в патче.

Объединение данных о покрытии кода

При параллельном запуске тестов или запуске в отдельных процессах каждый запуск может создать собственный сериализованный файл покрытия кода с помощью параметра --coverage-php PHPUnit. Команда phpcov merge объединяет эти файлы и формирует сводный отчёт о покрытии кода.

phpunit--coverage-php/tmp/coverage/FooTest.cov--filterFooTest
phpunit--coverage-php/tmp/coverage/BarTest.cov--filterBarTest

phpcovmerge--html/tmp/coverage-report/tmp/coverage

Команде phpcov merge требуется указать в качестве аргумента каталог, содержащий файлы *.cov. Необходимо указать как минимум один формат отчёта:

  • --clover <file> создаёт отчёт в формате Clover XML

  • --openclover <file> создаёт отчёт в формате OpenClover XML

  • --cobertura <file> создаёт отчёт в формате Cobertura XML

  • --crap4j <file> создаёт отчёт в формате Crap4J XML

  • --html <directory> создаёт отчёт в формате HTML

  • --php <file> экспортирует сериализованные данные о покрытии кода

  • --text <file> создаёт отчёт в текстовом формате

  • --xml <directory> создаёт отчёт в формате PHPUnit XML

За один запуск можно создать отчёты в нескольких форматах:

phpcovmerge--html/tmp/html--openclover/tmp/clover.xml/tmp/coverage

Параметр --source <directory> позволяет указать путь к исходному коду при объединении данных на компьютере, отличном от того, на котором эти данные о покрытии кода были собраны.

По умолчанию команда объединения требует, чтобы все файлы *.cov были созданы с использованием одной и той же версии PHP, одного и того же драйвера покрытия кода и совпадающей информации git. Эти требования можно смягчить с помощью следующих параметров:

  • --do-not-require-matching-git-information

  • --do-not-require-matching-php-version

  • --do-not-require-matching-code-coverage-driver

Покрытие патча

Команда phpcov patch-coverage вычисляет покрытие кода для изменённых строк в унифицированном diff. Это полезно, например, чтобы определить, покрыты ли тестами изменения в коммите.

gitdiffHEAD~1>/tmp/patch.txt
phpunit--coverage-php/tmp/coverage.cov

phpcovpatch-coverage/tmp/coverage.cov/tmp/patch.txt

В качестве аргументов этой команде требуются сериализованный файл покрытия кода (*.cov) и файл патча в формате унифицированного diff. Параметр --path-prefix <prefix> позволяет удалить префикс из путей в файле патча, чтобы они совпадали с путями в данных о покрытии кода.

Код завершения указывает результат:

  • 0 — все изменённые исполняемые строки покрыты

  • 1 — некоторые изменённые исполняемые строки не покрыты

  • 2 — изменённых исполняемых строк не обнаружено (это может указывать на несовпадение путей)

Ограничения

И Xdebug, и PCOV собирают информацию о покрытии кода на уровне байт-кода, а не исходного кода. При выполнении кода PHP сначала проходит этап компиляции: среда выполнения PHP компилирует исходный код PHP в байт-код PHP, который затем выполняется. При необходимости байт-код PHP может быть оптимизирован и кэширован (с JIT или без него). Xdebug и PCOV перехватывают выполнение этого байт-кода и отслеживают, какие опкоды действительно выполняются.

Это создаёт фундаментальную проблему сопоставления: скомпилированный байт-код не изоморфен исходному коду. При компиляции исходного кода PHP в байт-код PHP могут создаваться «искусственные» ветви, которых нет в исходном коде PHP.

Ещё одна важная проблема связана с оптимизатором байт-кода PHP. Параметр конфигурации PHP opcache.optimization_level представляет собой битовую маску, которая управляет применяемыми к байт-коду оптимизациями. Это приводит к неприятному последствию:

Сгенерированный байт-код зависит от оптимизаций, выполняемых OPcache. Один и тот же исходный код PHP может компилироваться в разный байт-код PHP в зависимости от настройки opcache.optimization_level. Из-за этого может оказаться невозможным точно сопоставить данные покрытия с исходным кодом. Поэтому отчёты о покрытии кода отражают выполнение байт-кода, а не логику исходного кода.

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

Spec-Zone.ru

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