Joel Barnard
Joel Barnard

Reputation: 23

PHPUnit: Calling a child method from abstract class constructor

I'm seeing an unexpected (to me) behaviour with PHPUnit, is this a bug, or am I doing something wrong? Simplified test case:

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

If I run the above code in PHPUnit, I get the following error:

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

Upvotes: 0

Views: 591

Answers (1)

Schleis
Schleis

Reputation: 43800

Change your mock call to:

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

Though this is an odd example for a test. Typically, we don't want to create a mock of the class that we are testing. And creating an object to pass into the class that we are mocking is kind of backwards to me.

You called your test case abstractTest but you are testing the concrete child of the abstract class. If your intention is to test abstractSpeaker then you would just use getMockForAbstractClass like so:

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

However this introduces the problem that setSpeaker is an abstract method that you are calling in your constructor and we are not able to mock that. Personally, I think that you are doing too much in the example constructor and would remove it. Which would end up like this:

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');
}

Though in this case, there really is no need to use the setters and getters at all. And you could just pass the speaker to the method and call the speak method on it directly.

If you were intending to test the concrete child, you could do it like this:

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');
}

Upvotes: 1

Related Questions