PHPUnit9.0 代碼覆蓋率分析

2022-03-22 14:59 更新

在本章中,你將學(xué)到 PHPUnit 中與代碼覆蓋率相關(guān)的一切功能。通過這部分功能,能夠了解在測試運(yùn)行過程中執(zhí)行了生產(chǎn)代碼的哪些部分。它使用了 php-code-coverage 組件,而這個(gè)組件又使用了 PHP 的 Xdebug PCOV 擴(kuò)展或 PHPDBG 所提供的代碼覆蓋率功能。

PHPUnit 可以生成基于 HTML 的代碼覆蓋率報(bào)告,同時(shí)也能生成好幾種(Clover、Crap4J、PHPUnit)基于 XML 的代碼覆蓋率信息記錄文件。代碼覆蓋率信息也能以文本格式提供(同時(shí)可以輸出到 STDOUT)或以 PHP 代碼格式輸出以供進(jìn)一步處理。

用于代碼覆蓋率的軟件衡量標(biāo)準(zhǔn)

目前存在多種軟件衡量標(biāo)準(zhǔn)用于衡量代碼覆蓋率:

  • 行覆蓋率(?Line Coverage?):按單個(gè)可執(zhí)行行是否已執(zhí)行進(jìn)行計(jì)量。
  • 分支覆蓋率(?Branch Coverage?):按控制結(jié)構(gòu)的分支進(jìn)行計(jì)量。測試套件運(yùn)行時(shí)每個(gè)控制結(jié)構(gòu)的布爾表達(dá)式求值為 ?true ?和 ?false ?各自計(jì)為一個(gè)分支。
  • 路徑覆蓋率(?Path Coverage?):按測試套件運(yùn)行時(shí)函數(shù)或者方法內(nèi)部所經(jīng)歷的執(zhí)行路徑進(jìn)行計(jì)量。一個(gè)執(zhí)行路徑指的是從進(jìn)入函數(shù)或方法一直到離開的過程中經(jīng)過各個(gè)分支的特定序列。
  • 函數(shù)與方法覆蓋率(?Function and Method Coverage?):按單個(gè)函數(shù)或方法是否已調(diào)用進(jìn)行計(jì)量。僅當(dāng)函數(shù)或方法的所有可執(zhí)行行全部已覆蓋時(shí) php-code-coverage 才將其視為已覆蓋。
  • 類與特質(zhì)覆蓋率(?Class and Trait Coverage?):按單個(gè)類或特質(zhì)的所有方法是否全部已覆蓋進(jìn)行計(jì)量。僅當(dāng)一個(gè)類或特質(zhì)的所有方法全部已覆蓋時(shí) php-code-coverage 才將其視為已覆蓋。
  • 變更風(fēng)險(xiǎn)反模式(CRAP)指數(shù)(?Change Risk Anti-Patterns (CRAP) Index?):是基于代碼單元的圈復(fù)雜度(cyclomatic complexity)與代碼覆蓋率計(jì)算得出的。不太復(fù)雜并具有恰當(dāng)測試覆蓋率的代碼將得出較低的 CRAP 指數(shù)??梢酝ㄟ^編寫測試或重構(gòu)代碼來降低其復(fù)雜性的方式來降低 CRAP 指數(shù)。

包含文件

為了告訴 PHPUnit 哪些源代碼文件要包含在代碼覆蓋率報(bào)告中,必須配置過濾器??梢杂妹钚羞x項(xiàng) ?--coverage-filter? 或通過配置文件來完成。

?includeUncoveredFilesInCodeCoverageReport ?和 ?processUncoveredFilesForCodeCoverageReport ?配置設(shè)置可用于配置過濾器的使用方式:

  • ?includeUncoveredFilesInCodeCoverageReport="false"? 意味著只有至少有一行已執(zhí)行代碼的文件才會包括在代碼覆蓋率報(bào)告中
  • ?includeUncoveredFilesInCodeCoverageReport="true"?(默認(rèn)值)意味著所有文件都會包括在代碼覆蓋率報(bào)告中,即使文件中沒有任何一行代碼被執(zhí)行過也一樣
  • ?processUncoveredFilesForCodeCoverageReport="false"?(默認(rèn)值)意味著沒有已執(zhí)行代碼行的文件會被加入到代碼覆蓋率報(bào)告中(如果設(shè)置了 ?includeUncoveredFilesInCodeCoverageReport="true"?),但它并不會被 PHPUnit 加載,因此也不會對其進(jìn)行分析來獲取正確的可執(zhí)行代碼行信息
  • ?processUncoveredFilesForCodeCoverageReport="true"? 意味著沒有已執(zhí)行代碼行的文件會被 PHPUnit 加載,從而也能對其進(jìn)行分析來獲取正確的可執(zhí)行代碼行信息

請注意,當(dāng)設(shè)置了 ?processUncoveredFilesForCodeCoverageReport="true"? 時(shí)將對源代碼文件進(jìn)行載入,這在某些情況下可能導(dǎo)致問題,比如,源代碼文件包含有處于類或者函數(shù)作用域之外的代碼。

忽略代碼塊

有時(shí),一些代碼塊是無法對其進(jìn)行測試的,因此希望在代碼覆蓋率分析中忽略它們。在 PHPUnit 中可以用 ?@codeCoverageIgnore?、?@codeCoverageIgnoreStart? 與 ?@codeCoverageIgnoreEnd? 標(biāo)注來做到這點(diǎn),如示例 9.1 中所示。
示例 9.1 使用 ?@codeCoverageIgnore?、?@codeCoverageIgnoreStart? 和 ?@codeCoverageIgnoreEnd? 標(biāo)注

<?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

代碼中被忽略掉的行(用標(biāo)注標(biāo)記為忽略)將會計(jì)為已執(zhí)行(如果它們是可執(zhí)行的),并且不會在代碼覆蓋情況中被高亮標(biāo)記。

指明覆蓋的代碼部分

?@covers? 標(biāo)注可以用在測試代碼中來指明測試類(或測試方法)想要對哪些代碼部分進(jìn)行測試。如果提供了這個(gè)信息,則可以有效過濾代碼覆蓋率報(bào)告,僅包含所指定的代碼部分中的已執(zhí)行代碼。示例 9.2 展示了一個(gè)例子。

如果用 ?@covers? 標(biāo)注指定了一個(gè)方法嗎,那么只有所指方法會被視為已覆蓋,這個(gè)方法所調(diào)用的方法不會視為已覆蓋。因此,如果用提取方法重構(gòu)了已覆蓋的方法,則需要添加相應(yīng)的 ?@covers? 標(biāo)注。這就是推薦將此標(biāo)注用在類作用域而非方法作用域的原因。

示例 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 指明了要覆蓋哪個(gè)方法的測試

<?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());
    }
}

同時(shí),可以用 ?@coversNothing? 標(biāo)注來指明一個(gè)測試不覆蓋任何方法。這可以在編寫集成測試時(shí)用于確保代碼覆蓋全部來自單元測試。
示例 9.4 指明應(yīng)當(dāng)不覆蓋任何方法的測試

<?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);
    }
}

邊緣情況

本節(jié)中展示了一些值得注意的邊緣情況,在這些邊緣情況中可能出現(xiàn)令人迷惑的代碼覆蓋率信息。

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

// 因?yàn)槭恰盎谛小钡亩腔谡Z句的覆蓋率
// 一行始終只能有一種覆蓋狀態(tài)
if (false) this_function_call_shows_up_as_covered();

// 由于代碼覆蓋率的內(nèi)部工作方式,這兩行顯得很特殊。
// 這一行會顯示為非可執(zhí)行
if (false)
    // 這一行會顯示為已覆蓋,
    // 實(shí)際上是上一行的 if 語句的覆蓋信息顯示在這了!
    will_also_show_up_as_covered();

// 要避免這種情況,必須使用大括號
if (false) {
    this_call_will_never_show_up_as_covered();
}


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號