PHPUnit9.0 測試替身-Stubs(樁件)

2022-05-11 17:19 更新

將對象替換為(可選地)返回配置好的返回值的測試替身的實踐方法稱為打樁(stubbing)??梢杂脴都⊿tub)來“替換掉被測系統(tǒng)所依賴的實際組件,這樣測試就有了對被測系統(tǒng)的間接輸入的控制點。這使得測試能強制安排被測系統(tǒng)的執(zhí)行路徑,否則被測系統(tǒng)可能無法執(zhí)行”。

示例 8.2 展示了如何對方法的調(diào)用進行上樁以及如何設(shè)定返回值。首先用 ?PHPUnit\Framework\TestCase? 類提供的 ?createStub()? 方法來建立一個樁件對象,它表面看起來像是 ?SomeClass? 類(示例 8.1)的實例。隨后用 PHPUnit 提供的流暢式接口來指定樁件的行為。本質(zhì)上,這意味著不需要建立多個臨時對象然后再把它們捆到一起。取而代之的是范例中所示的鏈式方法調(diào)用。這使得代碼更加易讀并更加“流暢”。

示例 8.1 想要上樁的類

<?php declare(strict_types=1);
class SomeClass
{
    public function doSomething()
    {
        // 隨便做點什么。
    }
}

示例 8.2 對某個方法的調(diào)用進行上樁,返回固定值

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

final class StubTest extends TestCase
{
    public function testStub(): void
    {
        // 為 SomeClass 類創(chuàng)建樁件。
        $stub = $this->createStub(SomeClass::class);

        // 配置樁件。
        $stub->method('doSomething')
             ->willReturn('foo');

        // 現(xiàn)在調(diào)用 $stub->doSomething() 會返回 'foo'。
        $this->assertSame('foo', $stub->doSomething());
    }

僅當原始類中不包含名字為“method”的方法時,以上范例才能正常運行。
如果原始類包含名為“method”的方法,就必須用 ?$stub->expects($this->any())->method('doSomething')->willReturn('foo');?

“在幕后”,當使用了 ?createStub()? 方法時, PHPUnit 自動生成了一個新的 PHP 類來實現(xiàn)想要的行為。
請注意:?createStub()? 會自動遞歸地基于方法的返回類型對返回值進行上樁??紤]以下示例:
示例 8.3 帶有返回類型聲明的方法

<?php declare(strict_types=1);
class C
{
    public function m(): D
    {
        // 隨便做點什么。
    }
}

在上述示例中,?C::m()? 方法具有返回類型聲明,指示此方法返回類型為 ?D? 的對象。那么,舉個例子說,創(chuàng)建 ?C? 的測試替身而又未用 ?willReturn()? 給 ?m()? 配置返回值時,則當 PHPUnit 調(diào)用 ?m()? 時會自動創(chuàng)建一個 ?D ?的測試替身作為返回值。
類似地,如果 ?m ?的返回類型聲明是標量類型,則會生成諸如 ?0?(對于 ?int?)、?0.0?(對于 ?float?)、或 ?[]?(對于 ?array?)這樣的返回值。
示例 8.4 展示了如何用仿件生成器的流暢式接口來配置測試替身的生成。這個測試替身的默認配置用的是和 ?createStub()? 相同的最佳實踐。
示例 8.4 使用可用于配置生成的測試替身類的仿件生成器 API

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

final class StubTest extends TestCase
{
    public function testStub(): void
    {
        // 為 SomeClass 類創(chuàng)建樁件。
        $stub = $this->getMockBuilder(SomeClass::class)
                     ->disableOriginalConstructor()
                     ->disableOriginalClone()
                     ->disableArgumentCloning()
                     ->disallowMockingUnknownTypes()
                     ->getMock();

        // 配置樁件。
        $stub->method('doSomething')
             ->willReturn('foo');

        // 現(xiàn)在調(diào)用 $stub->doSomething() 會返回 'foo'。
        $this->assertSame('foo', $stub->doSomething());
    }

在之前的例子中,用 ?willReturn($value) ?返回簡單值。這個簡短的語法相當于 ?will($this->returnValue($value))?。而在這個長點的語法中,可以使用變量,從而實現(xiàn)更復(fù)雜的上樁行為。
有時想要將(未改變的)方法調(diào)用時所使用的參數(shù)之一作為樁件的方法的調(diào)用結(jié)果來返回。示例 8.5 展示了如何用 ?returnArgument()? 代替 ?returnValue()? 來做到這點。
示例 8.5 對某個方法的調(diào)用進行上樁,返回參數(shù)之一

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

final class StubTest extends TestCase
{
    public function testReturnArgumentStub(): void
    {
        // 為 SomeClass 類創(chuàng)建樁件。
        $stub = $this->createStub(SomeClass::class);

        // 配置樁件。
        $stub->method('doSomething')
             ->will($this->returnArgument(0));

        // $stub->doSomething('foo') 返回 'foo'
        $this->assertSame('foo', $stub->doSomething('foo'));

        // $stub->doSomething('bar') 返回 'bar'
        $this->assertSame('bar', $stub->doSomething('bar'));
    }
}

在用流暢式接口進行測試時,讓某個已上樁的方法返回對樁件對象的引用有時會很有用。示例 8.6 展示了如何用 ?returnSelf()? 來做到這點。
示例 8.6 對方法的調(diào)用進行上樁,返回對樁件對象的引用

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

final class StubTest extends TestCase
{
    public function testReturnSelf(): void
    {
        // 為 SomeClass 類創(chuàng)建樁件。
        $stub = $this->createStub(SomeClass::class);

        // 配置樁件。
        $stub->method('doSomething')
             ->will($this->returnSelf());

        // $stub->doSomething() 返回 $stub
        $this->assertSame($stub, $stub->doSomething());
    }
}

有時候,上樁的方法需要根據(jù)預(yù)定義的參數(shù)清單來返回不同的值。可以用 ?returnValueMap()? 方法將參數(shù)和相應(yīng)的返回值關(guān)聯(lián)起來建立映射。示例參見示例 8.7。
示例 8.7 對方法的調(diào)用進行上樁,按照映射確定返回值

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

final class StubTest extends TestCase
{
    public function testReturnValueMapStub(): void
    {
        // 為 SomeClass 類創(chuàng)建樁件。
        $stub = $this->createStub(SomeClass::class);

        // Create a map of arguments to return values.
        $map = [
            ['a', 'b', 'c', 'd'],
            ['e', 'f', 'g', 'h']
        ];

        // 配置樁件。
        $stub->method('doSomething')
             ->will($this->returnValueMap($map));

        // $stub->doSomething() 根據(jù)提供的參數(shù)返回不同的值。
        $this->assertSame('d', $stub->doSomething('a', 'b', 'c'));
        $this->assertSame('h', $stub->doSomething('e', 'f', 'g'));
    }
}

如果上樁的方法需要返回計算得到的值而不是固定值(參見 ?returnValue()?)或某個(未改變的)參數(shù)(參見 ?returnArgument()?),可以用 ?returnCallback()? 來讓上樁的方法返回回調(diào)函數(shù)或方法的結(jié)果。示例參見示例 8.8。
示例 8.8 對方法的調(diào)用進行上樁,由回調(diào)生成返回值

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

final class StubTest extends TestCase
{
    public function testReturnCallbackStub(): void
    {
        // 為 SomeClass 類創(chuàng)建樁件。
        $stub = $this->createStub(SomeClass::class);

        // 配置樁件。
        $stub->method('doSomething')
             ->will($this->returnCallback('str_rot13'));

        // $stub->doSomething($argument) 返回 str_rot13($argument)
        $this->assertSame('fbzrguvat', $stub->doSomething('something'));
    }
}

相比于建立回調(diào)方法,有一個更簡單的選擇是直接給出期望返回值的列表??梢杂??onConsecutiveCalls()? 方法來做到這個。示例參見示例 8.9。
示例 8.9 對方法的調(diào)用上樁,按照指定順序返回列表中的值

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

final class StubTest extends TestCase
{
    public function testOnConsecutiveCallsStub(): void
    {
        // 為 SomeClass 類創(chuàng)建樁件。
        $stub = $this->createStub(SomeClass::class);

        // 配置樁件。
        $stub->method('doSomething')
             ->will($this->onConsecutiveCalls(2, 3, 5, 7));

        // $stub->doSomething() 每次都會返回不同的值
        $this->assertSame(2, $stub->doSomething());
        $this->assertSame(3, $stub->doSomething());
        $this->assertSame(5, $stub->doSomething());
    }
}

除了返回一個值之外,上樁的方法還能拋出一個異常。示例 8.10 展示了如何用 ?throwException()? 做到這點。
示例 8.10 對方法的調(diào)用進行上樁,拋出異常

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

final class StubTest extends TestCase
{
    public function testThrowExceptionStub(): void
    {
        // 為 SomeClass 類創(chuàng)建樁件。
        $stub = $this->createStub(SomeClass::class);

        // 配置樁件。
        $stub->method('doSomething')
             ->will($this->throwException(new Exception));

        // $stub->doSomething() 拋出異常
        $stub->doSomething();
    }
}

另外,也可以自行編寫樁件,并在此過程中改善設(shè)計。在系統(tǒng)中被廣泛使用的資源是通過單個外觀(facade)來訪問的,因此就能用樁件替換掉資源。例如,將散落在代碼各處的對數(shù)據(jù)庫的直接調(diào)用替換為單個 ?Database ?對象,這個對象實現(xiàn)了 ?IDatabase ?接口。接下來,就可以創(chuàng)建實現(xiàn)了 ?IDatabase ?的樁件并在測試中使用之。甚至可以創(chuàng)建一個選項來控制是用樁件還是用真實數(shù)據(jù)庫來運行測試,這樣測試就既能在開發(fā)過程中用作本地測試,又能在實際數(shù)據(jù)庫環(huán)境中進行集成測試。
需要上樁的功能往往集中在同一個對象中,這就改善了內(nèi)聚度。將功能通過單一且一致的接口呈現(xiàn)出來,就降低了這部分與系統(tǒng)其他部分之間的耦合度。


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號