Leogout
Leogout

Reputation: 1247

instanceof extended interfaces with PhpUnit

The context:

I have this interface which extends three other interfaces:

interface BasicTagsInterface extends TitleTagInterface, DescriptionTagInterface, KeywordsTagInterface {}

This is the method i'd like to test with PhpUnit:

public function fromResource($resource)
{
    if ($resource instanceof TitleTagInterface) {
        $this->setTitle($resource->getSeoTitle());
    }
    if ($resource instanceof DescriptionTagInterface) {
        $this->setDescription($resource->getSeoDescription());
    }
    if ($resource instanceof KeywordsTagInterface) {
        $this->setKeywords($resource->getSeoKeywords());
    }
}

And this is how I tried to test it so far:

$resource = $this->getMockBuilder(BasicTagsInterface::class)->setMethods([
    'getSeoTitle',
    'getSeoDescription',
    'getSeoKeywords',
])->getMock();

$resource->method('getSeoTitle')->willReturn('Awesome site');
$resource->method('getSeoDescription')->willReturn('My awesome site is so cool!');
$resource->method('getSeoKeywords')->willReturn('awesome, cool');
// TagBuilder::fromResource() is the method above
$this->tagBuilder->fromResource($resource);

I've also tried:

class A implements BasicTagsInterface {
    public function getSeoDescription(){}
    public function getSeoKeywords(){}
    public function getSeoTitle(){}
}

// And in the test method:
$resource = $this->getMockBuilder(A::class) // etc.

The problem: With this configuration, $resource instanseof BasicTagsInterface returns true and the other instanceof tests return false.

My question: How can I mock an interface which extends other interfaces and get the instanceof tests on the extended interfaces return true as they should outside a test case ?

Upvotes: 1

Views: 992

Answers (1)

dbrumann
dbrumann

Reputation: 17166

I can't find any assertions in your test, but for me everything works fine:

interface Title { function getTitle(); }
interface Description { function getDescription(); }
interface Resource extends Description, Title {}

class MyObject {
    public $title;
    public $description;

    function fromResource(Resource $resource) {
        if ($resource instanceof Title) {
            $this->setTitle($resource->getTitle());
        }
        if ($resource instanceof Description) {
            $this->setDescription($resource->getDescription());
        }
    }

    function setTitle($title) { $this->title = $title; }
    function setDescription($description) { $this->description = $description; }
}

class Question43426479Test extends TestCase {
    function testFromResource() {
        $resourceMock = $this->getMockBuilder(Resource::class)->getMock();
        $resourceMock->method('getTitle')->willReturn('SomeTitle');
        $resourceMock->method('getDescription')->willReturn('SomeDescription');

        $object = new MyObject();
        $object->fromResource($resourceMock);

        $this->assertEquals('SomeTitle', $object->title);
        $this->assertEquals('SomeDescription', $object->description);
    }
}

When I run the test I get the following output:

.                                                                   1 / 1 (100%)

Time: 66 ms, Memory: 8.00MB

OK (1 test, 2 assertions)

As you can see my test has 2 assertions. I don't test for the interface types, because I'm passing a dummy object anyway and I don't care about it's interfaces, but whether it produced the correct results when passed to my Object-class' fromResource method. Instead I assert for the expected outcome in that class, i.e. whether the setters were called.

If you want to test whether your Resource is of a specific type, I would instead create a class implementing the interface:

class MyResource implements Resource
{
    ...
}

and then write a separate test ensuring that when I construct a MyResource it is an instance of the desired interfaces:

function testResourceImplementsTitleAndDescription()
{
    $resource = new MyResource();

    $this->assertInstanceOf(Title::class, $resource);
    $this->assertInstanceOf(Description::class, $resource);
}

As you can see I don't use a Mock here, because I want to know about the actual object not a dummy created in it's place.

Upvotes: 2

Related Questions