仿件對象(Mock Object)

2018-02-24 15:42 更新

仿件對象(Mock Object)

將對象替換為能驗證預(yù)期行為(例如斷言某個方法必會被調(diào)用)的測試替身的實踐方法稱為模仿(mocking)。

可以用 仿件對象(mock object)“作為觀察點來核實被測試系統(tǒng)在測試中的間接輸出。通常,仿件對象還需要包括樁件的功能,因為如果測試尚未失敗則仿件對象需要向被測系統(tǒng)返回一些值,但是其重點還是在對間接輸出的核實上。因此,仿件對象遠(yuǎn)不止是樁件加斷言,它是以一種從根本上完全不同的方式來使用的”(Gerard Meszaros)。

局限性:對預(yù)期的自動校驗

PHPUnit只會對在某個測試的作用域內(nèi)生成的仿件對象進行自動校驗。諸如在數(shù)據(jù)供給器內(nèi)生成或用@depends 標(biāo)注注入測試的仿件對象,PHPUnit并不會自動對其進行校驗。

這有個例子:假設(shè)需要測試的當(dāng)前方法,在例子中是 update(),確實在一個觀察著另外一個對象的對象中上被調(diào)用了。Example?9.10, “被測系統(tǒng)(SUT)中 Subject 與 Observer 類的代碼”展示了被測系統(tǒng)(SUT)中 SubjectObserver 兩個類的代碼。

Example?9.10.?被測系統(tǒng)(SUT)中 Subject 與 Observer 類的代碼

<?php
class Subject
{
    protected $observers = array();
    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()
    {
        // 做點什么。
        // ...

        // 通知觀察者。
        $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)
    {
        // 做點什么。
    }

    public function reportError($errorCode, $errorMessage, Subject $subject)
    {
        // 做點什么。
    }

    // 其他方法。
}
?>

Example?9.11, “測試某個方法會以特定參數(shù)被調(diào)用一次”展示了如何用仿件對象來測試 SubjectObserver 對象之間的互動。

首先用 PHPUnit_Framework_TestCase 類提供的 getMock() 方法建立 Observer 的仿件對象。由于給出了一個數(shù)組做為 getMock() 方法的第二(可選)參數(shù),Observer 類只有 update() 方法會被替換為仿實現(xiàn)。

由于關(guān)注的是檢驗?zāi)硞€方法是否被調(diào)用,以及調(diào)用時具體所使用的參數(shù),因此引入 expects()with() 方法來指明此交互應(yīng)該是什么樣的。

Example?9.11.?測試某個方法會以特定參數(shù)被調(diào)用一次

<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
    public function testObserversAreUpdated()
    {
        // 為 Observer 類建立仿件對象,只模仿 update() 方法。
        $observer = $this->getMockBuilder('Observer')
                         ->setMethods(array('update'))
                         ->getMock();

        // 建立預(yù)期狀況:update() 方法將會被調(diào)用一次,
        // 并且將以字符串 'something' 為參數(shù)。
        $observer->expects($this->once())
                 ->method('update')
                 ->with($this->equalTo('something'));

        // 創(chuàng)建 Subject 對象,并將模仿的 Observer 對象連接其上。
        $subject = new Subject('My subject');
        $subject->attach($observer);

        // 在 $subject 對象上調(diào)用 doSomething() 方法,
        // 預(yù)期將以字符串 'something' 為參數(shù)調(diào)用 
        // Observer 仿件對象的 update() 方法。
        $subject->doSomething();
    }
}
?>

with() 方法可以攜帶任何數(shù)量的參數(shù),對應(yīng)于被模仿的方法的參數(shù)數(shù)量??梢詫Ψ椒ǖ膮?shù)指定更加高等的約束而不僅是簡單的匹配。

Example?9.12.?測試某個方法將會以特定數(shù)量的參數(shù)進行調(diào)用,并且對各個參數(shù)以多種方式進行約束

<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
    public function testErrorReported()
    {
        // 為 Observer 類建立仿件,對 reportError() 方法進行模仿
        $observer = $this->getMockBuilder('Observer')
                         ->setMethods(array('reportError'))
                         ->getMock();

        $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)會通過(observer的)reportError()方法
        //向 observer 報告錯誤。
        $subject->doSomethingBad();
    }
}
?>

withConsecutive() 方法可以接受任意多個數(shù)組作為參數(shù),具體數(shù)量取決于欲測試的調(diào)用。每個數(shù)組都都是對被仿方法的相應(yīng)參數(shù)的一組約束,就像 with() 中那樣。

Example?9.13.?測試某個方法將會以特定參數(shù)被調(diào)用二次

<?php
class FooTest extends PHPUnit_Framework_TestCase
{
    public function testFunctionCalledTwoTimesWithSpecificArguments()
    {
        $mock = $this->getMockBuilder('stdClass')
                     ->setMethods(array('set'))
                     ->getMock();

        $mock->expects($this->exactly(2))
             ->method('set')
             ->withConsecutive(
                 array($this->equalTo('foo'), $this->greaterThan(0)),
                 array($this->equalTo('bar'), $this->greaterThan(0))
             );

        $mock->set('foo', 21);
        $mock->set('bar', 48);
    }
}
?>

callback() 約束用來進行更加復(fù)雜的參數(shù)校驗。此約束的唯一參數(shù)是一個 PHP 回調(diào)項(callback)。此 PHP 回調(diào)項接受需要校驗的參數(shù)作為其唯一參數(shù),并應(yīng)當(dāng)在參數(shù)通過校驗時返回 TRUE,否則返回 FALSE。

Example?9.14.?更加復(fù)雜的參數(shù)校驗

<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
    public function testErrorReported()
    {
        // 為 Observer 類建立仿件,模仿 reportError() 方法
        $observer = $this->getMockBuilder('Observer')
                         ->setMethods(array('reportError'))
                         ->getMock();

        $observer->expects($this->once())
                 ->method('reportError')
                 ->with($this->greaterThan(0),
                        $this->stringContains('Something'),
                        $this->callback(function($subject){
                          return is_callable(array($subject, 'getName')) &&
                                 $subject->getName() == 'My subject';
                        }));

        $subject = new Subject('My subject');
        $subject->attach($observer);

        // doSomethingBad() 方法應(yīng)當(dāng)會通過(observer的)reportError()方法
        //向 observer 報告錯誤。
        $subject->doSomethingBad();
    }
}
?>

Example?9.15.?測試某個方法將會被調(diào)用一次,并且以某個特定對象作為參數(shù)。

<?php
class FooTest extends PHPUnit_Framework_TestCase
{
    public function testIdenticalObjectPassed()
    {
        $expectedObject = new stdClass;

        $mock = $this->getMockBuilder('stdClass')
                     ->setMethods(array('foo'))
                     ->getMock();

        $mock->expects($this->once())
             ->method('foo')
             ->with($this->identicalTo($expectedObject));

        $mock->foo($expectedObject);
    }
}
?>

Example?9.16.?創(chuàng)建仿件對象時啟用參數(shù)克隆

<?php
class FooTest extends PHPUnit_Framework_TestCase
{
    public function testIdenticalObjectPassed()
    {
        $cloneArguments = true;

        $mock = $this->getMockBuilder('stdClass')
                     ->enableArgumentCloning()
                     ->getMock();

        // 現(xiàn)在仿件將對參數(shù)進行克隆,因此 identicalTo 約束將會失敗。
    }
}
?>

Table?A.1, “約束條件”列出了可以應(yīng)用于方法參數(shù)的各種約束,Table?9.1, “匹配器”列出了可以用于指定調(diào)用次數(shù)的各種匹配器。

Table?9.1.?匹配器

匹配器 含義
PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount any() 返回一個匹配器,當(dāng)被評定的方法執(zhí)行0次或更多次(即任意次數(shù))時匹配成功。
PHPUnit_Framework_MockObject_Matcher_InvokedCount never() 返回一個匹配器,當(dāng)被評定的方法從未執(zhí)行時匹配成功。
PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce atLeastOnce()` 返回一個匹配器,當(dāng)被評定的方法執(zhí)行至少一次時匹配成功。
PHPUnit_Framework_MockObject_Matcher_InvokedCount once() 返回一個匹配器,當(dāng)被評定的方法執(zhí)行恰好一次時匹配成功。
PHPUnit_Framework_MockObject_Matcher_InvokedCount exactly(int $count) 返回一個匹配器,當(dāng)被評定的方法執(zhí)行恰好 $count 次時匹配成功。
PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex at(int $index) 返回一個匹配器,當(dāng)被評定的方法是第 $index 個執(zhí)行的方法時匹配成功。

Note

at() 匹配器的 $index 參數(shù)指的是對給定仿件對象的所有方法的調(diào)用的索引,從零開始。使用這個匹配器要謹(jǐn)慎,因為它可能導(dǎo)致測試由于與具體的實現(xiàn)細(xì)節(jié)過分緊密綁定而變得脆弱。

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號