Spec-Zone.ru › PHPUnit 8

Анализ покрытия кода

Википедия:

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

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

Примечание

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

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

Обратитесь к Исполнителю тестов командной строки для получения списка переключателей командной строки, которые управляют функциональностью покрытия кода, а также к Элементу <logging> для соответствующих параметров конфигурации.

Метрики программного обеспечения для покрытия кода

Существуют различные метрики программного обеспечения для измерения покрытия кода:

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

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

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

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

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

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

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

Метрика программного обеспечения Покрытие OPCODE измеряет, был ли выполнен каждый opcode функции или метода при выполнении набора тестов. Одна строка кода обычно компилируется в более чем один opcode. Покрытие строк считает строку кода покрытой, как только один из её opcodes выполнен.

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

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

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

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

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

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

Примечание

Метрики программного обеспечения Покрытие OPCODE, Покрытие ветвей и Покрытие путей пока не поддерживаются php-code-coverage.

Белый список файлов

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

Доступны параметры конфигурации addUncoveredFilesFromWhitelist и processUncoveredFilesFromWhitelist для настройки использования белого списка:

  • addUncoveredFilesFromWhitelist="false" означает, что только файлы из белого списка, имеющие по крайней мере одну строку выполненного кода, включаются в отчёт о покрытии кода
  • addUncoveredFilesFromWhitelist="true" (по умолчанию) означает, что все файлы из белого списка включаются в отчёт о покрытии кода, даже если ни одна строка кода в таких файлах не выполняется
  • processUncoveredFilesFromWhitelist="false" (по умолчанию) означает, что файл из белого списка, у которого нет выполненных строк кода, будет добавлен в отчёт о покрытии кода (если addUncoveredFilesFromWhitelist="true" установлено), но он не будет загружен PHPUnit и, следовательно, не будет проанализирован на правильность исполняемых строк кода
  • processUncoveredFilesFromWhitelist="true" означает, что файл из белого списка, у которого нет выполненных строк кода, будет загружен PHPUnit, чтобы он мог быть проанализирован на правильность исполняемых строк кода

Примечание

Обратите внимание, что загрузка файлов исходного кода, выполняемая при установке processUncoveredFilesFromWhitelist="true", может вызвать проблемы, если файл исходного кода содержит код за пределами области класса или функции.

Игнорирование блоков кода

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

Пример 9.1 Использование аннотаций @codeCoverageIgnore, @codeCoverageIgnoreStart и @codeCoverageIgnoreEnd
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

/**
 * @codeCoverageIgnore
 */
final class Foo
{
    public function bar(): void
    {
    }
}

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

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

exit; // @codeCoverageIgnore

Игнорируемые строки кода (отмеченные как игнорируемые с помощью аннотаций) считаются выполненными (если они исполняемы) и не будут выделены.

Указание покрытых частей кода

Аннотация @covers (см. документацию по аннотациям) может использоваться в коде теста для указания тех частей кода, которые класс теста (или метод теста) хочет протестировать. Если указано, это эффективно фильтрует отчёт о покрытии кода, чтобы включить только выполненный код из указанных частей кода. Пример 9.2 показывает пример.

Примечание

Если метод указан с аннотацией @covers, то только указанный метод будет считаться покрытым, а не методы, вызываемые этим методом. Следовательно, при рефакторинге покрытого метода с помощью рефакторинга выделение метода, необходимо добавить соответствующие аннотации @covers. По этой причине рекомендуется использовать эту аннотацию с классовым, а не с методовым объёмом.

Пример 9.2 Класс тестов, который указывает, какой класс он хочет покрыть
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

/**
 * @covers \Invoice
 * @uses \Money
 */
final class InvoiceTest extends TestCase
{
    private $invoice;

    protected function setUp(): void
    {
        $this->invoice = new Invoice;
    }

    public function testAmountInitiallyIsEmpty(): void
    {
        $this->assertEquals(new Money, $this->invoice->getAmount());
    }
}
Пример 9.3 Тесты, которые указывают, какой метод они хотят покрыть
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class BankAccountTest extends TestCase
{
    private $ba;

    protected function setUp(): void
    {
        $this->ba = new BankAccount;
    }

    /**
     * @covers \BankAccount::getBalance
     */
    public function testBalanceIsInitiallyZero(): void
    {
        $this->assertSame(0, $this->ba->getBalance());
    }

    /**
     * @covers \BankAccount::withdrawMoney
     */
    public function testBalanceCannotBecomeNegative(): void
    {
        try {
            $this->ba->withdrawMoney(1);
        }

        catch (BankAccountException $e) {
            $this->assertSame(0, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    /**
     * @covers \BankAccount::depositMoney
     */
    public function testBalanceCannotBecomeNegative2(): void
    {
        try {
            $this->ba->depositMoney(-1);
        }

        catch (BankAccountException $e) {
            $this->assertSame(0, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    /**
     * @covers \BankAccount::getBalance
     * @covers \BankAccount::depositMoney
     * @covers \BankAccount::withdrawMoney
     */
    public function testDepositWithdrawMoney(): void
    {
        $this->assertSame(0, $this->ba->getBalance());
        $this->ba->depositMoney(1);
        $this->assertSame(1, $this->ba->getBalance());
        $this->ba->withdrawMoney(1);
        $this->assertSame(0, $this->ba->getBalance());
    }
}

Также возможно указать, что тест не должен покрывать никакой метод, используя аннотацию @coversNothing (см. @coversNothing). Это может быть полезно при написании интеграционных тестов, чтобы убедиться, что вы генерируете покрытие кода только с помощью модульных тестов.

Пример 9.4 Тест, который указывает, что не должен покрывать ни один метод
<?php declare(strict_types=1);
use PHPUnit\DbUnit\TestCase

final class GuestbookIntegrationTest extends TestCase
{
    /**
     * @coversNothing
     */
    public function testAddEntry(): void
    {
        $guestbook = new Guestbook();
        $guestbook->addEntry("suzy", "Hello world!");

        $queryTable = $this->getConnection()->createQueryTable(
            'guestbook', 'SELECT * FROM guestbook'
        );

        $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml")
                              ->getTable("guestbook");

        $this->assertTablesEqual($expectedTable, $queryTable);
    }
}

Крайние случаи

Этот раздел демонстрирует примечательные крайние случаи, которые приводят к запутанной информации о покрытии кода.

<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

// Because it is "line based" and not statement base coverage
// one line will always have one coverage status
if (false) this_function_call_shows_up_as_covered();

// Due to how code coverage works internally these two lines are special.
// This line will show up as non executable
if (false)
    // This line will show up as covered because it is actually the
    // coverage of the if statement in the line above that gets shown here!
    will_also_show_up_as_covered();

// To avoid this it is necessary that braces are used
if (false) {
    this_call_will_never_show_up_as_covered();
}

Ускорение покрытия кода с помощью Xdebug

Производительность сбора данных о покрытии кода с помощью Xdebug 2.6 (и более поздних версий) может быть значительно улучшена путём делегирования фильтрации белого списка Xdebug.

Для этого первым шагом является генерация скрипта фильтра для Xdebug с помощью опции --dump-xdebug-filter:

$ phpunit --dump-xdebug-filter build/xdebug-filter.php
PHPUnit 7.4.0 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.2.11 with Xdebug 2.6.1
Configuration: /workspace/project/phpunit.xml

Wrote Xdebug filter script to build/xdebug-filter.php

Теперь мы можем использовать опцию --prepend для загрузки скрипта фильтра Xdebug как можно раньше, когда мы хотим сгенерировать отчёт о покрытии кода:

$ phpunit --prepend build/xdebug-filter.php --coverage-html build/coverage-report

© 2005–2020 Sebastian Bergmann
Licensed under the Creative Commons Attribution 3.0 Unported License.
https://phpunit.readthedocs.io/en/8.5/code-coverage-analysis.html

Spec-Zone.ru

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