PHPUnit9.0 基境(fixture)

2022-03-24 09:08 更新

在編寫測(cè)試時(shí),最費(fèi)時(shí)的部分之一是編寫代碼來將整個(gè)場(chǎng)景設(shè)置成某個(gè)已知的狀態(tài),并在測(cè)試結(jié)束后將其復(fù)原到初始狀態(tài)。這個(gè)已知的狀態(tài)稱為測(cè)試的基境(?fixture?)。

在用 PHPUnit 測(cè)試數(shù)組操作中,基境就是存儲(chǔ)在 ?$stack? 變量中的數(shù)組。然而,絕大多數(shù)時(shí)候基境均遠(yuǎn)比一個(gè)簡(jiǎn)單數(shù)組要復(fù)雜,用于建立基境的代碼量也會(huì)隨之增長(zhǎng)。測(cè)試的真正內(nèi)容就被淹沒于建立基境帶來的干擾中。當(dāng)編寫多個(gè)需要類似基境的測(cè)試時(shí)這個(gè)問題就變得更糟糕了。如果沒有來自于測(cè)試框架的幫助,就不得不在寫每一個(gè)測(cè)試時(shí)都將建立基境的代碼重復(fù)一次。

PHPUnit 支持共享建立基境的代碼。在運(yùn)行某個(gè)測(cè)試方法前,會(huì)調(diào)用一個(gè)名叫 ?setUp()? 的模板方法。?setUp()? 是創(chuàng)建測(cè)試所用對(duì)象的地方。當(dāng)測(cè)試方法運(yùn)行結(jié)束后,不管是成功還是失敗,都會(huì)調(diào)用另外一個(gè)名叫 ?tearDown()? 的模板方法。?tearDown()? 是清理測(cè)試所用對(duì)象的地方。

在用 ?@depends? 標(biāo)注來表示依賴關(guān)系中,我們?cè)跍y(cè)試之間運(yùn)用生產(chǎn)者-消費(fèi)者關(guān)系來共享基境。這并非總是預(yù)期的方式,甚至有時(shí)是不可能的。示例 4.1 展示了另外一個(gè)編寫測(cè)試 ?StackTest ?的方式。在這個(gè)方式中,不再重用基境本身,而是重用建立基境的代碼。首先聲明一個(gè)實(shí)例變量,?$stack?,用來替代方法內(nèi)的局部變量。然后把 ?array ?基境的建立放到 ?setUp()? 方法中。最后,從測(cè)試方法中去除冗余代碼,在 ?assertSame()? 斷言方法中使用新引入的實(shí)例變量 ?$this->stack? 替代方法內(nèi)的局部變量 ?$stack?。

示例 4.1 用 setUp() 來創(chuàng)建堆棧基境

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

final class StackTest extends TestCase
{
    private $stack;

    protected function setUp(): void
    {
        $this->stack = [];
    }

    public function testEmpty(): void
    {
        $this->assertTrue(empty($this->stack));
    }

    public function testPush(): void
    {
        array_push($this->stack, 'foo');

        $this->assertSame('foo', $this->stack[count($this->stack)-1]);
        $this->assertFalse(empty($this->stack));
    }

    public function testPop(): void
    {
        array_push($this->stack, 'foo');

        $this->assertSame('foo', array_pop($this->stack));
        $this->assertTrue(empty($this->stack));
    }
}

測(cè)試類的每個(gè)測(cè)試方法都會(huì)運(yùn)行一次 ?setUp()? 和 ?tearDown()? 模板方法(同時(shí),每個(gè)測(cè)試方法都是在一個(gè)全新的測(cè)試類實(shí)例上運(yùn)行的)。
另外,?setUpBeforeClass()? 與 ?tearDownAfterClass()? 模板方法將分別在測(cè)試用例類的第一個(gè)測(cè)試運(yùn)行之前和測(cè)試用例類的最后一個(gè)測(cè)試運(yùn)行之后調(diào)用。
下面這個(gè)例子中展示了測(cè)試用例類中所有可用的模板方法。

示例 4.2 展示所有可用模板方法的示例

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

final class TemplateMethodsTest extends TestCase
{
    public static function setUpBeforeClass(): void
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    protected function setUp(): void
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    protected function assertPreConditions(): void
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    public function testOne(): void
    {
        fwrite(STDOUT, __METHOD__ . "\n");
        $this->assertTrue(true);
    }

    public function testTwo(): void
    {
        fwrite(STDOUT, __METHOD__ . "\n");
        $this->assertTrue(false);
    }

    protected function assertPostConditions(): void
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    protected function tearDown(): void
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    public static function tearDownAfterClass(): void
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    protected function onNotSuccessfulTest(Throwable $t): void
    {
        fwrite(STDOUT, __METHOD__ . "\n");
        throw $t;
    }
}
$ phpunit TemplateMethodsTest
PHPUnit latest.0 by Sebastian Bergmann and contributors.

TemplateMethodsTest::setUpBeforeClass
TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testOne
TemplateMethodsTest::assertPostConditions
TemplateMethodsTest::tearDown
.TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testTwo
TemplateMethodsTest::tearDown
TemplateMethodsTest::onNotSuccessfulTest
FTemplateMethodsTest::tearDownAfterClass

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) TemplateMethodsTest::testTwo
Failed asserting that <boolean:false> is true.
/home/sb/TemplateMethodsTest.php:30

FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

setUp() 多、tearDown() 少

理論上說,?setUp()? 和 ?tearDown()? 是精確對(duì)稱的,但是實(shí)踐中并非如此。實(shí)際上,只有在 ?setUp(?) 中分配了諸如文件或套接字之類的外部資源時(shí)才需要實(shí)現(xiàn) ?tearDown()? 。如果 ?setUp()? 中只創(chuàng)建純 PHP 對(duì)象,通??梢月赃^ ?tearDown()?。不過,如果在 ?setUp()? 中創(chuàng)建了大量對(duì)象,你可能想要在 ?tearDown()? 中 ?unset()? 指向這些對(duì)象的變量,這樣它們就可以被垃圾回收機(jī)制回收掉。對(duì)測(cè)試用例對(duì)象的垃圾回收動(dòng)作則是不可預(yù)知的。

變體

如果擁有兩個(gè)測(cè)試,它們的基境建立工作略有不同,該怎么辦?有兩種可能:

  • 如果兩個(gè) ?setUp()? 代碼僅有微小差異,把有差異的代碼內(nèi)容從 ?setUp()? 移到測(cè)試方法內(nèi)。
  • 如果兩個(gè) ?setUp()? 是確實(shí)不一樣,那么需要另外一個(gè)測(cè)試用例類。參考基境建立工作的不同之處來命名這個(gè)類。

基境共享

有幾個(gè)好的理由來在測(cè)試之間共享基境,但是大部分情況下,在測(cè)試之間共享基境的需求都源于某個(gè)未解決的設(shè)計(jì)問題。
一個(gè)有實(shí)際意義的多測(cè)試間共享基境的例子是數(shù)據(jù)庫連接:只登錄數(shù)據(jù)庫一次,然后重用此連接,而不是每個(gè)測(cè)試都建立一個(gè)新的數(shù)據(jù)庫連接。這樣能加快測(cè)試的運(yùn)行。
示例 4.3 中用 ?setUpBeforeClass()? 和 ?tearDownAfterClass()? 模板方法來分別在測(cè)試用例類的第一個(gè)測(cè)試之前和最后一個(gè)測(cè)試之后連接與斷開數(shù)據(jù)庫。
示例 4.3 在同一個(gè)測(cè)試套件內(nèi)的不同測(cè)試之間共享基境

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

final class DatabaseTest extends TestCase
{
    private static $dbh;

    public static function setUpBeforeClass(): void
    {
        self::$dbh = new PDO('sqlite::memory:');
    }

    public static function tearDownAfterClass(): void
    {
        self::$dbh = null;
    }
}

需要反復(fù)強(qiáng)調(diào)的是:在測(cè)試之間共享基境會(huì)降低測(cè)試的價(jià)值。潛在的設(shè)計(jì)問題是對(duì)象之間并非松散耦合。如果解決掉潛在的設(shè)計(jì)問題并使用樁件(stub)來編寫測(cè)試,就能達(dá)成更好的結(jié)果,而不是在測(cè)試之間產(chǎn)生運(yùn)行時(shí)依賴并錯(cuò)過改進(jìn)設(shè)計(jì)的機(jī)會(huì)。

全局狀態(tài)

使用單件(singleton)的代碼很難測(cè)試。使用全局變量的代碼也一樣。通常情況下,欲測(cè)代碼和全局變量之間會(huì)強(qiáng)烈耦合,并且其創(chuàng)建無法控制。另外一個(gè)問題是,一個(gè)測(cè)試對(duì)全局變量的改變可能會(huì)破壞另外一個(gè)測(cè)試。
在 PHP 中,全局變量是這樣運(yùn)作的:

  • 全局變量 ?$foo = 'bar'; ?實(shí)際上是存儲(chǔ)為 ?$GLOBALS['foo'] = 'bar'; ?的。
  • ?$GLOBALS?這個(gè)變量是一種被稱為超全局變量的變量。
  • 超全局變量是一種在任何變量作用域中都總是可用的內(nèi)建變量。
  • 在函數(shù)或者方法的變量作用域中,要訪問全局變量 ?$foo?,可以直接訪問 ?$GLOBALS['foo']?,或者用 ?global $foo;? 來創(chuàng)建一個(gè)引用全局變量的局部變量。

除了全局變量,類的靜態(tài)屬性也是一種全局狀態(tài)。
在版本 6 之前,默認(rèn)情況下,PHPUnit 用一種更改全局變量與超全局變量(?$GLOBALS?、?$_ENV?、?$_POST?、?$_GET?、?$_COOKIE?、?$_SERVER?、?$_FILES?、?$_REQUEST?)不會(huì)影響到其他測(cè)試的方式來運(yùn)行所有測(cè)試。
在版本 6 中,默認(rèn)情況下 PHPUnit 不再對(duì)全局變量和超全局變量進(jìn)行這種備份與恢復(fù)的操作。可以用 ?--globals-backup? 選項(xiàng)或在 XML 配置文件中用 ?backupGlobals="true"? 將其激活。
通過用 ?--static-backup? 選項(xiàng)或在 XML 配置文件中設(shè)置 ?backupStaticAttributes="true"?,可以將此隔離擴(kuò)展到類的靜態(tài)屬性。

注解:

對(duì)全局變量和類的靜態(tài)屬性的備份與還原操作使用了 ?serialize()? 與 ?unserialize()?
某些類的實(shí)例對(duì)象(比如 ?PDO?)無法序列化,因此如果把這樣一個(gè)對(duì)象存放在比如說 ?$GLOBALS? 數(shù)組內(nèi)時(shí),備份操作就會(huì)出問題。

 ?@backupGlobals? 標(biāo)注可以用來控制對(duì)全局變量的備份與還原操作。另外,還可以提供一個(gè)全局變量的名單,名單中的全局變量將被排除于備份與還原操作之外,就像這樣:

final class MyTest extends TestCase
{
    protected $backupGlobalsExcludeList = ['globalVariable'];

    // ...
}

注解:

在方法(例如 ?setUp()? 方法)內(nèi)對(duì) ?$backupGlobalsBlacklist? 屬性進(jìn)行設(shè)置是無效的。

?@backupStaticAttributes? 標(biāo)注可以用于在每個(gè)測(cè)試之前備份所有已聲明類的靜態(tài)屬性值并在其后恢復(fù)。
它所處理的并不只是測(cè)試類自身,而是在測(cè)試開始時(shí)已聲明的所有類。它只作用于靜態(tài)類屬性,不作用于函數(shù)內(nèi)聲明的靜態(tài)變量。

注解:

只有啟用了 ?@backupStaticAttributes? 的測(cè)試方法才會(huì)在方法之前執(zhí)行此操作。如果在此之前運(yùn)行的某個(gè)沒有啟用 ?@backupStaticAttributes? 的測(cè)試方法改變了靜態(tài)屬性的值,那么被備份及還原的將會(huì)是這個(gè)改變后的值——而非初始聲明時(shí)提供的默認(rèn)值。PHP 并不額外記錄任何靜態(tài)變量的聲明時(shí)提供的初始默認(rèn)值。
同樣的情況也發(fā)生于測(cè)試內(nèi)部新加載/聲明的類的靜態(tài)屬性上。它們也無法在測(cè)試結(jié)束之后復(fù)原為聲明時(shí)提供的原始默認(rèn)值,因?yàn)闊o從得知這些默認(rèn)值。這些被修改過的值會(huì)泄漏到后繼測(cè)試中。
對(duì)單元測(cè)試而言,推薦在 ?setUp()? 中顯式的重置測(cè)試中使用到的靜態(tài)屬性(最好同時(shí)在 ?tearDown()? 中執(zhí)行重置,這樣就保證不會(huì)影響到后繼的測(cè)試)。

可以提供名單來將靜態(tài)屬性從備份與還原操作中排除出去:

final class MyTest extends TestCase
{
    protected $backupStaticAttributesExcludeList = [
        'className' => ['attributeName']
    ];

    // ...
}

注解:

在方法(例如 ?setUp()? 方法)內(nèi)對(duì) ?$backupStaticAttributesExcludeList? 屬性進(jìn)行設(shè)置是無效的。


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)