Анализ покрытия кода
Wikipedia:
В информатике покрытие кода — это мера, используемая для описания степени, в которой исходный код программы тестируется конкретным набором тестов. Программа с высоким покрытием кода была более тщательно протестирована и имеет меньшую вероятность содержать ошибки программного обеспечения, чем программа с низким покрытием кода.
В этой главе вы узнаете все о функциональности покрытия кода PHPUnit, которая даёт представление о том, какие части кода приложения выполняются при запуске тестов. Она использует компонент php-code-coverage, который, в свою очередь, использует функциональность покрытия кода, предоставляемую расширениями Xdebug или PCOV для PHP или PHPDBG.
Примечание
Если при запуске тестов вы видите предупреждение о том, что драйвер покрытия кода недоступен, это означает, что вы используете бинарный файл PHP CLI (php) и не загружены Xdebug или PCOV.
PHPUnit может генерировать отчёт о покрытии кода в формате HTML, а также файлы журналов в формате XML с информацией о покрытии кода в различных форматах (Clover, Crap4J, PHPUnit). Информация о покрытии кода также может быть представлена в текстовом формате (и выведена в STDOUT) и экспортирована в виде кода PHP для дальнейшей обработки.
Обратитесь к Инструменту командной строки для запуска тестов за списком командно-строковых переключателей, которые управляют функциональностью покрытия кода, а также к Элементу <logging> для соответствующих параметров конфигурации.
Метрики программного обеспечения для покрытия кода
Существуют различные метрики программного обеспечения для измерения покрытия кода:
Покрытие строк
Метрика Покрытие строк измеряет, была ли выполнена каждая исполняемая строка.
Покрытие ветвей
Метрика Покрытие ветвей измеряет, оценивалось ли булево выражение каждой структуры управления какtrueиfalseпри выполнении набора тестов.
Покрытие путей
Метрика Покрытие путей измеряет, были ли пройдены все возможные пути выполнения в функции или методе при выполнении набора тестов. Путь выполнения — это уникальная последовательность ветвей от входа в функцию или метод до его выхода.
Покрытие функций и методов
Метрика Покрытие функций и методов измеряет, был ли вызван каждый метод или функция. php-code-coverage рассматривает функцию или метод как покрытый только тогда, когда все его исполняемые строки покрыты.
Покрытие классов и трейтов
Метрика Покрытие классов и трейтов измеряет, покрыты ли все методы класса или трейта. php-code-coverage считает класс или трейт покрытым только тогда, когда покрыты все его методы.
Индекс антипаттернов риска изменений (CRAP)
Индекс антипаттернов риска изменений (CRAP) вычисляется на основе цикломатической сложности и покрытия кода единицы кода. Код, который не слишком сложен и имеет достаточное покрытие тестами, будет иметь низкий индекс CRAP. Индекс CRAP можно снизить, написав тесты и переделав код для снижения его сложности.
Включение файлов
Обязательно следует настроить фильтр, чтобы указать PHPUnit, какие файлы исходного кода следует включить в отчёт о покрытии кода. Это можно сделать, используя опцию командной строки --coverage-filter или в файле конфигурации (см. Элемент <include>).
Параметры конфигурации includeUncoveredFilesInCodeCoverageReport и processUncoveredFilesForCodeCoverageReport доступны для настройки способа использования фильтра:
-
includeUncoveredFilesInCodeCoverageReport="false"означает, что в отчёт о покрытии кода включаются только файлы, в которых выполнена хотя бы одна строка кода. -
includeUncoveredFilesInCodeCoverageReport="true"(по умолчанию) означает, что в отчёт о покрытии кода включаются все файлы, даже если ни одна строка кода в них не выполняется. -
processUncoveredFilesForCodeCoverageReport="false"(по умолчанию) означает, что файл, у которого нет выполненных строк кода, будет добавлен в отчёт о покрытии кода (еслиincludeUncoveredFilesInCodeCoverageReport="true"задано), но он не будет загружен PHPUnit и, следовательно, не будет проанализирован на наличие правильных исполняемых строк кода. -
processUncoveredFilesForCodeCoverageReport="true"означает, что файл, у которого нет выполненных строк кода, будет загружен PHPUnit, чтобы он мог быть проанализирован на наличие правильных исполняемых строк кода.
Примечание
Обратите внимание, что загрузка файлов исходного кода, выполняемая при установке processUncoveredFilesForCodeCoverageReport="true", может вызвать проблемы, если файл исходного кода содержит код, выходящий за рамки класса или функции.
Игнорирование блоков кода
Иногда у вас есть блоки кода, которые невозможно протестировать, и которые вы можете захотеть проигнорировать при анализе покрытия кода. PHPUnit позволяет это сделать с помощью аннотаций @codeCoverageIgnore, @codeCoverageIgnoreStart и @codeCoverageIgnoreEnd, как показано в Примере 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. По этой причине рекомендуется использовать эту аннотацию на уровне класса, а не на уровне метода.
<?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());
}
}
<?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). Это может быть полезно при написании интеграционных тестов, чтобы убедиться, что вы генерируете покрытие кода только с помощью модульных тестов.
<?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();
}
© 2005–2020 Sebastian Bergmann
Licensed under the Creative Commons Attribution 3.0 Unported License.
https://phpunit.readthedocs.io/en/9.5/code-coverage-analysis.html