將對象替換為(可選地)返回配置好的返回值的測試替身的實踐方法稱為打樁(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)其他部分之間的耦合度。
更多建議: