GordonM
GordonM

Reputation: 31770

PHPUnit: Stubbing multiple interfaces

I'm getting to grips with PHPUnit, and have so far found it pretty easy to use, but I've run up against a test case that's causing me difficulty.

I'm writing code against a set of interfaces that objects are expected to implement (some PHP ones, some self-made) and the SUT requires an input object to implement several interfaces. For example:

class MyClass implements ArrayAccess, MyInterface
{
    // ...
}

The SUT does thing such as this:

class ClassToBeTested
{
    protected $obj = NULL;

    public function __construct ($obj)
    {
        $this -> obj = $obj;
    }

    public function methodToBeTested ()
    {
        if ($this -> obj instanceof ArrayAccess)
        && ($this -> obj instanceof MyInterface)
        {
            // ...
        }
    }

    public function otherMethodUnderTest ()
    {
        if ($this -> obj instanceof ArrayAccess)
        {
            // ...
        }
        else
        if ($this -> obj instanceof MyInterface)
        {
            // ...
        }
    }
}

I can create a stub from one interface or the other, but I don't know if you can create a stub that implements them both.

protected function setUp ()
{
    $stubField  = $this -> getMockBuilder ('ArrayAccess')
            -> getMock ();
    $this -> object = new ClassToBeTested ($stubField);
}

or

protected function setUp ()
{
    $stubField  = $this -> getMockBuilder ('MyInterface')
            -> getMock ();
    $this -> object = new ClassToBeTested ($stubField);
}

Is it possible to generate stubs from a list of interfaces, or do I have to stub a concrete class that implements the expected interfaces? That in itself is causing difficulty, because the class that needs to be stubbed itself needs another object to be passed to its constructor, and I can't seem to get either disableOriginalConstructor () or setConstructorArgs () to work I think this is because the concrete classes in question don't implement the constructor themselves but inherit it from a superclass. Am I missing something obvious here?

Upvotes: 14

Views: 5669

Answers (4)

Olivier
Olivier

Reputation: 18260

PHPUnit 10 (Feb 2023) introduced the createMockForIntersectionOfInterfaces() and createStubForIntersectionOfInterfaces() TestCase methods (in 348ffd6d; #5122 etc.) that re-introduced an API to create a mock that implements multiple interfaces.

Previously this feature was available until PHPUnit 9 (excluding, removed in ab5b024a; deprecated as of 8.5 #3955)

Example taken from the documentation:

interface X
{
    public function m(): bool;
}
interface Y
{
    public function n(): int;
}
class Z
{
    public function doSomething(X&Y $input): bool
    {
        $result = false;

        // ...

        return $result;
    }
}
use PHPUnit\Framework\TestCase;

class MockForIntersectionExampleTest extends TestCase
{
    public function testCreateMockForIntersection(): void
    {
        $o = $this->createMockForIntersectionOfInterfaces([X::class, Y::class]);

        // $o is of type X ...
        $this->assertInstanceOf(X::class, $o);

        // ... and $o is of type Y
        $this->assertInstanceOf(Y::class, $o);
    }
}

Upvotes: 10

skiliton
skiliton

Reputation: 111

For the future if somebody happens to see this answer this works for me in PHPUnit 7:

$mock = $this
  ->getMockBuilder([InterfaceA::class,InterfaceB::class])
  ->getMock();

Upvotes: 10

Maksim Kotlyar
Maksim Kotlyar

Reputation: 3947

It is not a good idea to create an interface in application codebase to make tests happy. I mean you can create this interface but it would better if you put it somewhere in the test code base. For example you can put the interface after the test case class in the file directly

To test two interfaces same time I created an interface in the test case file (it could be any other place)

interface ApiAwareAction implements ActionInterface, ApiAwareInterface
{
}

And after I did a mock of that class:

$this->getMock('Payum\Tests\ApiAwareAction');

Upvotes: 3

liquorvicar
liquorvicar

Reputation: 6126

Do you have access to edit the original code? If so I would create a new interface that extends both ArrayAccess and MyInterface. That way you should be able to stub/mock an object to test the method under test.

Upvotes: 7

Related Questions