PHPUnit9.0 测试替身-仿件对象(Mock Object)

2022-03-22 14:41 更新

将对象替换为能验证预期行为(例如断言某个方法必会被调用)的测试替身的实践方法称为模仿(mocking)。

可以用仿件对象(mock object)“作为观察点来核实被测试系统在测试中的间接输出。通常,仿件对象还需要包括桩件的功能,因为如果测试尚未失败则仿件对象需要向被测系统返回一些值,但是其重点还是在对间接输出的核实上。因此,仿件对象远不止是桩件加断言,它是以一种从根本上完全不同的方式来使用的”。

PHPUnit 只会对在某个测试的作用域内生成的仿件对象进行自动校验。诸如在数据供给器内生成或用 ​@depends​ 标注注入测试的仿件对象,PHPUnit 并不会自动对其进行校验。

这有个例子:假设需要测试的当前方法,在例子中是 ​update()​,确实在一个观察着另外一个对象的对象中上被调用了。示例 8.11 展示了被测系统(SUT)中 ​Subject ​和 ​Observer ​两个类的代码。
示例 8.11 被测系统(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()
    {
        // 随便做点什么。
        // ...

        // 通知观察者我们做了点什么。
        $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)
    {
        // 随便做点什么
    }

    // 其他方法。
}

示例 8.12 展示了如何用仿件对象来测试 ​Subject ​和 ​Observer ​对象之间的互动。
首先用 ​PHPUnit\Framework\TestCase​ 类提供的 ​createMock()​ 方法来为 ​Observer ​建立仿件对象。
由于关注的是检验某个方法是否被调用,以及调用时具体所使用的参数,因此引入 ​expects()​ 与 ​with()​ 方法来指明此交互应该是什么样的。
示例 8.12 测试某个方法会以特定参数被调用一次

<?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() 方法建立预期:
        // 只会以字符串 'something' 为参数调用一次。
        $observer->expects($this->once())
                 ->method('update')
                 ->with($this->equalTo('something'));

        // 建立 Subject 对象并且将模仿的 Observer 对象附加其上。
        $subject = new Subject('My subject');
        $subject->attach($observer);

        // 在 $subject 上调用 doSomething() 方法,
        // 我们预期会以字符串 'something' 调用模仿的 Observer
        // 对象的 update() 方法。
        $subject->doSomething();
    }
}

with()​ 方法可以携带任何数量的参数,对应于被模仿的方法的参数数量。可以对方法的参数指定更加高等的约束而不仅是简单的匹配。
示例 8.13 测试某个方法将会以特定数量的参数进行调用,并且对各个参数以多种方式进行约束

<?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() 方法应当会通过
        // reportError() 方法向 observer 报告错误。
        $subject->doSomethingBad();
    }
}

withConsecutive()​ 方法可以接受任意多个数组作为参数,具体数量取决于欲测试的调用。每个数组都都是对被仿方法的相应参数的一组约束,就像 ​with()​ 中那样。
示例 8.14 测试某个方法将会以特定参数被调用两次。

<?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()​ 约束用来进行更加复杂的参数校验。此约束的唯一参数是一个 PHP 回调项(callback)。此 PHP 回调项接受需要校验的参数作为其唯一参数,并应当在参数通过校验时返回 ​true​,否则返回 ​false​。
示例 8.15 更复杂的参数校验

<?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() 方法应当会通过
        // reportError() 方法向 observer 报告错误。
        $subject->doSomethingBad();
    }
}

示例 8.16 测试某个方法将会被调用一次,并且以某个特定对象作为参数。

<?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 创建仿件对象时启用参数克隆

<?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();

        // 现在仿件会克隆参数,因此 identicalTo 约束会失败。
    }
}

约束条件中列出了可以应用于方法参数的各种约束,表格 8.1 中列出了可以用于指定调用次数的各种匹配器。

表格 8.1 匹配器

 匹配器 含义 
 PHPUnit\Framework\MockObject\Matcher\AnyInvokedCount any()  返回一个匹配器,当被评定的方法执行0次或更多次(即任意次数)时匹配成功。
 ​PHPUnit\Framework\MockObject\Matcher\InvokedCount never()  返回一个匹配器,当被评定的方法从未执行时匹配成功。
 ​PHPUnit\Framework\MockObject\Matcher\InvokedAtLeastOnce atLeastOnce()  返回一个匹配器,当被评定的方法执行至少一次时匹配成功。
 ​PHPUnit\Framework\MockObject\Matcher\InvokedCount once()  返回一个匹配器,当被评定的方法执行恰好一次时匹配成功。
 ​PHPUnit\Framework\MockObject\Matcher\InvokedCount exactly(int $count)  返回一个匹配器,当被评定的方法执行恰好 ​$count​ 次时匹配成功。
 ​PHPUnit\Framework\MockObject\Matcher\InvokedAtIndex at(int $index)  返回一个匹配器,当被评定的方法是第 ​$index​ 个执行的方法时匹配成功。

at()​ 匹配器的 ​$index​ 参数指的是对给定仿件对象的所有方法的调用的索引,从零开始。使用这个匹配器要谨慎,因为它可能导致测试由于与具体的实现细节过分紧密绑定而变得脆弱。

如一开始提到的,如果 ​createStub()​ 和 ​createMock()​ 方法在生成测试替身时所使用的默认值不符合你的要求,则可以通过 ​getMockBuilder($type)​ 方法来用流畅式接口定制测试替身的生成过程。以下是仿件生成器所提供的方法列表:

  • setMethods(array $methods)​ 可以在仿件生成器对象上调用,来指定哪些方法将被替换为可配置的测试替身。其他方法的行为不会有所改变。如果调用 ​setMethods(null)​,那么没有方法会被替换。
  • 可以在仿件生成器对象上调用 ​setMethodsExcept(array $methods)​ 来指定哪些方法不被替换为可配置的测试替身,与此同时所有其他 public 方法都会被替换。​setMethods()​ 的作用则相反。
  • setConstructorArgs(array $args)​ 可用于向原版类的构造函数(默认情况下不会被替换为伪实现)提供参数数组。
  • setMockClassName($name)​ 可用于指定生成的测试替身类的类名。
  • disableOriginalConstructor()​ 参数可用于禁用对原版类的构造方法的调用。
  • disableOriginalClone()​ 可用于禁用对原版类的克隆方法的调用。
  • disableAutoload()​ 可用于在测试替身类的生成期间禁用 ​__autoload()​。


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号