4

给定以下实现:

class Foo {
    public function helper() {
        // Does something with external side effects, like updating
        // a database or altering the file system.
    }

    public function bar() {
        if ($this->helper() === FALSE) {
            throw new Exception(/* ... */);
        }
    }
}

我将如何进行单元测试Foo::bar()而不产生测试期间的副作用Foo::helper()

我知道我可以模拟Foo和存根Foo::helper()

public function testGoodBar() {
    $mock = $this->getMock('Foo', array('helper'));
    $this->expects($this->once())
        ->method('helper')
        ->will($this->returnValue(TRUE));

    $this->assertTrue($mock->bar());
}

...但这使得测试对引入其他可能有副作用的方法的代码更改开放。然后,如果在没有更新的情况下再次运行测试,测试本身就会产生永久性的副作用。

我也可以 mock Foo,这样它的所有方法都会被嘲笑并且不会产生副作用:

public function testGoodBar() {
    $mock = $this->getMock('Foo');
    $this->expects($this->once())
        ->method('helper')
        ->will($this->returnValue(TRUE));

    $this->assertTrue($mock->bar());
}

...但随后甚至Foo::bar()被嘲笑,这很糟糕,因为这是我们想要测试的方法。

我能想出的唯一解决方案是显式地模拟被测方法之外的所有方法:

public function testGoodBar() {
    $mock = $this->getMock('Foo', array_diff(
        get_class_methods('Foo'),
        'bar'
    ));

    $this->expects($this->once())
        ->method('helper')
        ->will($this->returnValue(TRUE));

    $this->assertTrue($mock->bar());
}

...但这看起来很笨拙,我觉得我错过了一些明显的东西。

4

2 回答 2

5

(在此答案中考虑问题下的评论。)

如果您要扩展一个唯一目的是产生副作用的类,我希望所有扩展代码也会产生副作用。因此,您必须在测试中考虑到这一点,并设置一个环境,您可以在其中测试具有副作用的代码(即:为该测试启动并运行一个 memcached 实例)。

如果您不想要这个(可以理解),最好以可模拟的方式将代码编写为副作用类的包装器。因此,您Foo::__construct接受产生副作用的类的实例或工厂,因此您可以在测试中模拟它以仅测试无副作用的代码。

于 2012-11-06T16:22:09.843 回答
-1

看起来您应该更关心测试 helper() 调用的方法,而不是在这种情况下关心 bar() 。

有时当一个方法做了很多事情时,最好将它分解并让其他方法来做这些事情。执行此操作时,您希望得到与以前相同的输出,但代码被分解成更易于管理的部分。首先测试最小的部分很重要。当您开始测试 bar() 时,您不需要进行太多测试,因此您不必一遍又一遍地模拟它。因为很多情况都会在 helper() 的支持方法中覆盖。

于 2012-11-06T15:49:24.227 回答