PHPUnit:从抽象 class 构造函数调用子方法

PHPUnit: Calling a child method from abstract class constructor

我发现 PHPUnit 出现了(对我而言)意外行为,这是一个错误,还是我做错了什么? 简化测试用例:

abstract class abstractSpeaker {

    public function __construct($param) {
        $this->setSpeaker($param);
        $this->getSpeaker()->speak();  //bad line, causes error
        $this->tellSpeakerToSpeak();   //this lines ok
    }

    abstract function setSpeaker($value);

    abstract function getSpeaker();

    function tellSpeakerToSpeak() {
        $this->getSpeaker()->speak();  //this line works, but is same as bad line
    }
}

class speaker extends abstractSpeaker {
    protected $speaker;

    function setSpeaker($value) {
        $this->speaker = $value;
    }

    function getSpeaker() {
        return $this->speaker;
    }
}

class SayHello {
    public function speak() { print "hello"; }
}


class abstractTest extends \PHPUnit_Framework_TestCase {
    public function testIndex() {
        $mock = $this->getMockBuilder(speaker::class)
            ->setConstructorArgs([new SayHello()])
            ->setMethods([])
            ->getMock();

        $mock->tellSpeakerToSpeak();
    }
}

如果我在 PHPUnit 中 运行 上面的代码,我得到以下错误:

Fatal error: Call to a member function speak() on a non-object in abstracttest.php on line 9

将您的模拟电话更改为:

    $mock = $this->getMockBuilder('speaker')
        ->setConstructorArgs([new SayHello()])
        ->setMethods([])
        ->getMock();

尽管这是一个奇怪的测试示例。通常,我们不想创建我们正在测试的 class 的模拟。创建一个 object 传递给我们正在嘲笑的 class 对我来说有点倒退。

您调用了测试用例 abstractTest,但您正在测试抽象 class 的具体 child。如果您的目的是测试 abstractSpeaker 那么您可以像这样使用 getMockForAbstractClass

    $mock = $this->getMockBuilder('abstractSpeaker')
        ->setConstructorArgs([new SayHello()])
        ->setMethods([])
        ->getMockForAbstractClass();

然而,这引入了一个问题,即 setSpeaker 是您在构造函数中调用的抽象方法,我们无法模拟它。就个人而言,我认为您在示例构造函数中做的太多了,会删除它。最终会像这样:

public function testIndex() {
    $mock = $this->getMockBuilder('abstractSpeaker')
        ->setMethods(['setSpeaker', 'getSpeaker'])
        ->getMockForAbstractClass();

    $mockSpeaker = $this->getMockBuilder('SayHello')
        ->setMethods(['speak'])
        ->getMock();

    $mockSpeaker->expects($this->once())
         ->method('speak')
         ->will($this->returnCallback(function() { print 'Hello' }));

    $mock->expects($this->once())
         ->method('setSpeaker')
         ->with($mockSpeaker);

    $mock->expects($this->once())
         ->method('getSpeaker')
         ->willReturn($mockSpeaker);

    $mock->tellSpeakerToSpeak($mockSpeaker);

    $this->expectOutputString('Hello');
}

虽然在这种情况下,确实完全没有必要使用setters和getters。您可以将扬声器传递给方法并直接调用 speak 方法。

如果您打算测试具体的 child,您可以这样做:

public function testIndex() {
    $mock = $this->getMockBuilder('SayHello')
        ->setMethods(['speak'])
        ->getMock();

    $mock->expects($this->once())
         ->method('speak')
         ->will($this->returnCallback(function() { print 'Hello' }));

   $sut = new speaker($mock);

   $this->expectOutputString('Hello');
}