將對象替換為能驗(yàn)證預(yù)期行為(例如斷言某個(gè)方法必會被調(diào)用)的測試替身的實(shí)踐方法稱為模仿(mocking)。
可以用仿件對象(mock object)“作為觀察點(diǎn)來核實(shí)被測試系統(tǒng)在測試中的間接輸出。通常,仿件對象還需要包括樁件的功能,因?yàn)槿绻麥y試尚未失敗則仿件對象需要向被測系統(tǒng)返回一些值,但是其重點(diǎn)還是在對間接輸出的核實(shí)上。因此,仿件對象遠(yuǎn)不止是樁件加斷言,它是以一種從根本上完全不同的方式來使用的”。
PHPUnit 只會對在某個(gè)測試的作用域內(nèi)生成的仿件對象進(jìn)行自動校驗(yàn)。諸如在數(shù)據(jù)供給器內(nèi)生成或用 ?@depends
? 標(biāo)注注入測試的仿件對象,PHPUnit 并不會自動對其進(jìn)行校驗(yàn)。
這有個(gè)例子:假設(shè)需要測試的當(dāng)前方法,在例子中是 ?update()
?,確實(shí)在一個(gè)觀察著另外一個(gè)對象的對象中上被調(diào)用了。示例 8.11 展示了被測系統(tǒng)(SUT)中 ?Subject
?和 ?Observer
?兩個(gè)類的代碼。
示例 8.11 被測系統(tǒng)(SUT)中 Subject 與 Observer 類的代碼
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
class Subject
{
protected $observers = [];
protected $name;
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function attach(Observer $observer)
{
$this->observers[] = $observer;
}
public function doSomething()
{
// 隨便做點(diǎn)什么。
// ...
// 通知觀察者我們做了點(diǎn)什么。
$this->notify('something');
}
public function doSomethingBad()
{
foreach ($this->observers as $observer) {
$observer->reportError(42, 'Something bad happened', $this);
}
}
protected function notify($argument)
{
foreach ($this->observers as $observer) {
$observer->update($argument);
}
}
// 其他方法。
}
class Observer
{
public function update($argument)
{
// 隨便做點(diǎn)什么。
}
public function reportError($errorCode, $errorMessage, Subject $subject)
{
// 隨便做點(diǎn)什么
}
// 其他方法。
}
示例 8.12 展示了如何用仿件對象來測試 ?Subject
?和 ?Observer
?對象之間的互動。
首先用 ?PHPUnit\Framework\TestCase
? 類提供的 ?createMock()
? 方法來為 ?Observer
?建立仿件對象。
由于關(guān)注的是檢驗(yàn)?zāi)硞€(gè)方法是否被調(diào)用,以及調(diào)用時(shí)具體所使用的參數(shù),因此引入 ?expects()
? 與 ?with()
? 方法來指明此交互應(yīng)該是什么樣的。
示例 8.12 測試某個(gè)方法會以特定參數(shù)被調(diào)用一次
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class SubjectTest extends TestCase
{
public function testObserversAreUpdated(): void
{
// 為 Observer 類建立仿件
// 只模仿 update() 方法。
$observer = $this->createMock(Observer::class);
// 為 update() 方法建立預(yù)期:
// 只會以字符串 'something' 為參數(shù)調(diào)用一次。
$observer->expects($this->once())
->method('update')
->with($this->equalTo('something'));
// 建立 Subject 對象并且將模仿的 Observer 對象附加其上。
$subject = new Subject('My subject');
$subject->attach($observer);
// 在 $subject 上調(diào)用 doSomething() 方法,
// 我們預(yù)期會以字符串 'something' 調(diào)用模仿的 Observer
// 對象的 update() 方法。
$subject->doSomething();
}
}
?with()
? 方法可以攜帶任何數(shù)量的參數(shù),對應(yīng)于被模仿的方法的參數(shù)數(shù)量??梢詫Ψ椒ǖ膮?shù)指定更加高等的約束而不僅是簡單的匹配。
示例 8.13 測試某個(gè)方法將會以特定數(shù)量的參數(shù)進(jìn)行調(diào)用,并且對各個(gè)參數(shù)以多種方式進(jìn)行約束
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class SubjectTest extends TestCase
{
public function testErrorReported(): void
{
// 為 Observer 類建立仿件,模仿 reportError() 方法
$observer = $this->createMock(Observer::class);
$observer->expects($this->once())
->method('reportError')
->with(
$this->greaterThan(0),
$this->stringContains('Something'),
$this->anything()
);
$subject = new Subject('My subject');
$subject->attach($observer);
// doSomethingBad() 方法應(yīng)當(dāng)會通過
// reportError() 方法向 observer 報(bào)告錯(cuò)誤。
$subject->doSomethingBad();
}
}
?withConsecutive()
? 方法可以接受任意多個(gè)數(shù)組作為參數(shù),具體數(shù)量取決于欲測試的調(diào)用。每個(gè)數(shù)組都都是對被仿方法的相應(yīng)參數(shù)的一組約束,就像 ?with()
? 中那樣。
示例 8.14 測試某個(gè)方法將會以特定參數(shù)被調(diào)用兩次。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class FooTest extends TestCase
{
public function testFunctionCalledTwoTimesWithSpecificArguments(): void
{
$mock = $this->getMockBuilder(stdClass::class)
->setMethods(['set'])
->getMock();
$mock->expects($this->exactly(2))
->method('set')
->withConsecutive(
[$this->equalTo('foo'), $this->greaterThan(0)],
[$this->equalTo('bar'), $this->greaterThan(0)]
);
$mock->set('foo', 21);
$mock->set('bar', 48);
}
}
?callback()
? 約束用來進(jìn)行更加復(fù)雜的參數(shù)校驗(yàn)。此約束的唯一參數(shù)是一個(gè) PHP 回調(diào)項(xiàng)(callback)。此 PHP 回調(diào)項(xiàng)接受需要校驗(yàn)的參數(shù)作為其唯一參數(shù),并應(yīng)當(dāng)在參數(shù)通過校驗(yàn)時(shí)返回 ?true
?,否則返回 ?false
?。
示例 8.15 更復(fù)雜的參數(shù)校驗(yàn)
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class SubjectTest extends TestCase
{
public function testErrorReported(): void
{
// 為 Observer 類建立仿件,模仿 reportError() 方法
$observer = $this->createMock(Observer::class);
$observer->expects($this->once())
->method('reportError')
->with(
$this->greaterThan(0),
$this->stringContains('Something'),
$this->callback(function($subject)
{
return is_callable([$subject, 'getName']) &&
$subject->getName() == 'My subject';
}
));
$subject = new Subject('My subject');
$subject->attach($observer);
// doSomethingBad() 方法應(yīng)當(dāng)會通過
// reportError() 方法向 observer 報(bào)告錯(cuò)誤。
$subject->doSomethingBad();
}
}
示例 8.16 測試某個(gè)方法將會被調(diào)用一次,并且以某個(gè)特定對象作為參數(shù)。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class FooTest extends TestCase
{
public function testIdenticalObjectPassed(): void
{
$expectedObject = new stdClass;
$mock = $this->getMockBuilder(stdClass::class)
->setMethods(['foo'])
->getMock();
$mock->expects($this->once())
->method('foo')
->with($this->identicalTo($expectedObject));
$mock->foo($expectedObject);
}
}
示例 8.17 創(chuàng)建仿件對象時(shí)啟用參數(shù)克隆
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class FooTest extends TestCase
{
public function testIdenticalObjectPassed(): void
{
$cloneArguments = true;
$mock = $this->getMockBuilder(stdClass::class)
->enableArgumentCloning()
->getMock();
// 現(xiàn)在仿件會克隆參數(shù),因此 identicalTo 約束會失敗。
}
}
約束條件中列出了可以應(yīng)用于方法參數(shù)的各種約束,表格 8.1 中列出了可以用于指定調(diào)用次數(shù)的各種匹配器。
表格 8.1 匹配器
匹配器 | 含義 |
? PHPUnit\Framework\MockObject\Matcher\AnyInvokedCount any() ? |
返回一個(gè)匹配器,當(dāng)被評定的方法執(zhí)行0次或更多次(即任意次數(shù))時(shí)匹配成功。 |
?PHPUnit\Framework\MockObject\Matcher\InvokedCount never() ? |
返回一個(gè)匹配器,當(dāng)被評定的方法從未執(zhí)行時(shí)匹配成功。 |
?PHPUnit\Framework\MockObject\Matcher\InvokedAtLeastOnce atLeastOnce() ? |
返回一個(gè)匹配器,當(dāng)被評定的方法執(zhí)行至少一次時(shí)匹配成功。 |
?PHPUnit\Framework\MockObject\Matcher\InvokedCount once() ? |
返回一個(gè)匹配器,當(dāng)被評定的方法執(zhí)行恰好一次時(shí)匹配成功。 |
?PHPUnit\Framework\MockObject\Matcher\InvokedCount exactly(int $count) ? |
返回一個(gè)匹配器,當(dāng)被評定的方法執(zhí)行恰好 ?$count ? 次時(shí)匹配成功。 |
?PHPUnit\Framework\MockObject\Matcher\InvokedAtIndex at(int $index) ? |
返回一個(gè)匹配器,當(dāng)被評定的方法是第 ?$index ? 個(gè)執(zhí)行的方法時(shí)匹配成功。 |
?
at()
? 匹配器的 ?$index
? 參數(shù)指的是對給定仿件對象的所有方法的調(diào)用的索引,從零開始。使用這個(gè)匹配器要謹(jǐn)慎,因?yàn)樗赡軐?dǎo)致測試由于與具體的實(shí)現(xiàn)細(xì)節(jié)過分緊密綁定而變得脆弱。
如一開始提到的,如果 ?createStub()
? 和 ?createMock()
? 方法在生成測試替身時(shí)所使用的默認(rèn)值不符合你的要求,則可以通過 ?getMockBuilder($type)
? 方法來用流暢式接口定制測試替身的生成過程。以下是仿件生成器所提供的方法列表:
setMethods(array $methods)
? 可以在仿件生成器對象上調(diào)用,來指定哪些方法將被替換為可配置的測試替身。其他方法的行為不會有所改變。如果調(diào)用 ?setMethods(null)
?,那么沒有方法會被替換。setMethodsExcept(array $methods)
? 來指定哪些方法不被替換為可配置的測試替身,與此同時(shí)所有其他 public 方法都會被替換。?setMethods()
? 的作用則相反。setConstructorArgs(array $args)
? 可用于向原版類的構(gòu)造函數(shù)(默認(rèn)情況下不會被替換為偽實(shí)現(xiàn))提供參數(shù)數(shù)組。setMockClassName($name)
? 可用于指定生成的測試替身類的類名。disableOriginalConstructor()
? 參數(shù)可用于禁用對原版類的構(gòu)造方法的調(diào)用。disableOriginalClone()
? 可用于禁用對原版類的克隆方法的調(diào)用。disableAutoload()
? 可用于在測試替身類的生成期間禁用 ?__autoload()
?。
更多建議: