將對(duì)象替換為(可選地)返回配置好的返回值的測(cè)試替身的實(shí)踐方法稱為打樁(stubbing)??梢杂脴都⊿tub)來(lái)“替換掉被測(cè)系統(tǒng)所依賴的實(shí)際組件,這樣測(cè)試就有了對(duì)被測(cè)系統(tǒng)的間接輸入的控制點(diǎn)。這使得測(cè)試能強(qiáng)制安排被測(cè)系統(tǒng)的執(zhí)行路徑,否則被測(cè)系統(tǒng)可能無(wú)法執(zhí)行”。
示例 8.2 展示了如何對(duì)方法的調(diào)用進(jìn)行上樁以及如何設(shè)定返回值。首先用 ?PHPUnit\Framework\TestCase
? 類提供的 ?createStub()
? 方法來(lái)建立一個(gè)樁件對(duì)象,它表面看起來(lái)像是 ?SomeClass
? 類(示例 8.1)的實(shí)例。隨后用 PHPUnit 提供的流暢式接口來(lái)指定樁件的行為。本質(zhì)上,這意味著不需要建立多個(gè)臨時(shí)對(duì)象然后再把它們捆到一起。取而代之的是范例中所示的鏈?zhǔn)椒椒ㄕ{(diào)用。這使得代碼更加易讀并更加“流暢”。
示例 8.1 想要上樁的類
<?php declare(strict_types=1);
class SomeClass
{
public function doSomething()
{
// 隨便做點(diǎn)什么。
}
}
示例 8.2 對(duì)某個(gè)方法的調(diào)用進(jìn)行上樁,返回固定值
<?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() 會(huì)返回 'foo'。
$this->assertSame('foo', $stub->doSomething());
}
僅當(dāng)原始類中不包含名字為“method”的方法時(shí),以上范例才能正常運(yùn)行。
如果原始類包含名為“method”的方法,就必須用 ?$stub->expects($this->any())->method('doSomething')->willReturn('foo');
?
“在幕后”,當(dāng)使用了 ?createStub()
? 方法時(shí), PHPUnit 自動(dòng)生成了一個(gè)新的 PHP 類來(lái)實(shí)現(xiàn)想要的行為。
請(qǐng)注意:?createStub()
? 會(huì)自動(dòng)遞歸地基于方法的返回類型對(duì)返回值進(jìn)行上樁??紤]以下示例:
示例 8.3 帶有返回類型聲明的方法
<?php declare(strict_types=1);
class C
{
public function m(): D
{
// 隨便做點(diǎn)什么。
}
}
在上述示例中,?C::m()
? 方法具有返回類型聲明,指示此方法返回類型為 ?D
? 的對(duì)象。那么,舉個(gè)例子說(shuō),創(chuàng)建 ?C
? 的測(cè)試替身而又未用 ?willReturn()
? 給 ?m()
? 配置返回值時(shí),則當(dāng) PHPUnit 調(diào)用 ?m()
? 時(shí)會(huì)自動(dòng)創(chuàng)建一個(gè) ?D
?的測(cè)試替身作為返回值。
類似地,如果 ?m
?的返回類型聲明是標(biāo)量類型,則會(huì)生成諸如 ?0
?(對(duì)于 ?int
?)、?0.0
?(對(duì)于 ?float
?)、或 ?[]
?(對(duì)于 ?array
?)這樣的返回值。
示例 8.4 展示了如何用仿件生成器的流暢式接口來(lái)配置測(cè)試替身的生成。這個(gè)測(cè)試替身的默認(rèn)配置用的是和 ?createStub()
? 相同的最佳實(shí)踐。
示例 8.4 使用可用于配置生成的測(cè)試替身類的仿件生成器 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() 會(huì)返回 'foo'。
$this->assertSame('foo', $stub->doSomething());
}
在之前的例子中,用 ?willReturn($value)
?返回簡(jiǎn)單值。這個(gè)簡(jiǎn)短的語(yǔ)法相當(dāng)于 ?will($this->returnValue($value))
?。而在這個(gè)長(zhǎng)點(diǎn)的語(yǔ)法中,可以使用變量,從而實(shí)現(xiàn)更復(fù)雜的上樁行為。
有時(shí)想要將(未改變的)方法調(diào)用時(shí)所使用的參數(shù)之一作為樁件的方法的調(diào)用結(jié)果來(lái)返回。示例 8.5 展示了如何用 ?returnArgument()
? 代替 ?returnValue()
? 來(lái)做到這點(diǎn)。
示例 8.5 對(duì)某個(gè)方法的調(diào)用進(jìn)行上樁,返回參數(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'));
}
}
在用流暢式接口進(jìn)行測(cè)試時(shí),讓某個(gè)已上樁的方法返回對(duì)樁件對(duì)象的引用有時(shí)會(huì)很有用。示例 8.6 展示了如何用 ?returnSelf()
? 來(lái)做到這點(diǎn)。
示例 8.6 對(duì)方法的調(diào)用進(jìn)行上樁,返回對(duì)樁件對(duì)象的引用
<?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());
}
}
有時(shí)候,上樁的方法需要根據(jù)預(yù)定義的參數(shù)清單來(lái)返回不同的值??梢杂??returnValueMap()
? 方法將參數(shù)和相應(yīng)的返回值關(guān)聯(lián)起來(lái)建立映射。示例參見(jiàn)示例 8.7。
示例 8.7 對(duì)方法的調(diào)用進(jìn)行上樁,按照映射確定返回值
<?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'));
}
}
如果上樁的方法需要返回計(jì)算得到的值而不是固定值(參見(jiàn) ?returnValue()
?)或某個(gè)(未改變的)參數(shù)(參見(jiàn) ?returnArgument()
?),可以用 ?returnCallback()
? 來(lái)讓上樁的方法返回回調(diào)函數(shù)或方法的結(jié)果。示例參見(jiàn)示例 8.8。
示例 8.8 對(duì)方法的調(diào)用進(jìn)行上樁,由回調(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)方法,有一個(gè)更簡(jiǎn)單的選擇是直接給出期望返回值的列表。可以用 ?onConsecutiveCalls()
? 方法來(lái)做到這個(gè)。示例參見(jiàn)示例 8.9。
示例 8.9 對(duì)方法的調(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() 每次都會(huì)返回不同的值
$this->assertSame(2, $stub->doSomething());
$this->assertSame(3, $stub->doSomething());
$this->assertSame(5, $stub->doSomething());
}
}
除了返回一個(gè)值之外,上樁的方法還能拋出一個(gè)異常。示例 8.10 展示了如何用 ?throwException()
? 做到這點(diǎn)。
示例 8.10 對(duì)方法的調(diào)用進(jìn)行上樁,拋出異常
<?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();
}
}
另外,也可以自行編寫(xiě)樁件,并在此過(guò)程中改善設(shè)計(jì)。在系統(tǒng)中被廣泛使用的資源是通過(guò)單個(gè)外觀(facade)來(lái)訪問(wèn)的,因此就能用樁件替換掉資源。例如,將散落在代碼各處的對(duì)數(shù)據(jù)庫(kù)的直接調(diào)用替換為單個(gè) ?Database
?對(duì)象,這個(gè)對(duì)象實(shí)現(xiàn)了 ?IDatabase
?接口。接下來(lái),就可以創(chuàng)建實(shí)現(xiàn)了 ?IDatabase
?的樁件并在測(cè)試中使用之。甚至可以創(chuàng)建一個(gè)選項(xiàng)來(lái)控制是用樁件還是用真實(shí)數(shù)據(jù)庫(kù)來(lái)運(yùn)行測(cè)試,這樣測(cè)試就既能在開(kāi)發(fā)過(guò)程中用作本地測(cè)試,又能在實(shí)際數(shù)據(jù)庫(kù)環(huán)境中進(jìn)行集成測(cè)試。
需要上樁的功能往往集中在同一個(gè)對(duì)象中,這就改善了內(nèi)聚度。將功能通過(guò)單一且一致的接口呈現(xiàn)出來(lái),就降低了這部分與系統(tǒng)其他部分之間的耦合度。
更多建議: