PHPUnit: Stubbing multiple interfaces
Asked Answered
R

4

13

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?

Rao answered 17/12, 2011 at 10:44 Comment(2)
@Ator: When that feature was removed, PHP 8 union and intersection types were mentioned in a comment but I could find no further reaction nor any issue that sheds more light for the re-introduction from the PHPUnit project itself. It now has a new API (see the answer by Oliver) and the old list in github.com/sebastianbergmann/phpunit/issues/3955 , not sure if stubs were supported earlier, now in PHPUnit 10 they are.Karelia
@Ator: I should add, that the answer does no discussion about the design issues Sebastian Bergman raised in the removal issue, which plays a role in some of the existing answers. E.g. vanilla PHP mock-interfaces that were given as answer may still appear to be the right choice., the new API may re-introduce the earlier mentioned design problems of the code under test. But please place your bounty as you see fit.Karelia
T
7

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.

Trinary answered 17/12, 2011 at 10:49 Comment(3)
+1 cos that's such a bloody obvious idea I should have thought of it. Still I'm going to leave it open a little while longer before accepting to see what others suggest.Rao
Personaly prefer @Maksim Kotlyar answer as introducing an interface in your application codebase as a stub for your test will uselessly polute your production code.Extrasystole
@YohanG. As it stands, possibly yes. Looking at this again (after over 4.5 years!) I think the problem the OP has is actually a symptom of a deeper problem. The $obj he's passing in to the CUT can either implement ArrayAccess or MyInterface. It looks to me that there's another concept trying to get out. What he should be doing is acepting one Interface/Class in his CUT and allowing that to abstract the difference between the two. Thus get rid of those horrible if...instanceof blocks and be more polymorphic.Trinary
H
10

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();
Hara answered 31/3, 2018 at 19:0 Comment(2)
You can also do $this->createMock([A::class, B::class])Amplify
As of PHPUnit 8.5 this is no longer possible: github.com/sebastianbergmann/phpunit/issues/3955Rauwolfia
T
7

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.

Trinary answered 17/12, 2011 at 10:49 Comment(3)
+1 cos that's such a bloody obvious idea I should have thought of it. Still I'm going to leave it open a little while longer before accepting to see what others suggest.Rao
Personaly prefer @Maksim Kotlyar answer as introducing an interface in your application codebase as a stub for your test will uselessly polute your production code.Extrasystole
@YohanG. As it stands, possibly yes. Looking at this again (after over 4.5 years!) I think the problem the OP has is actually a symptom of a deeper problem. The $obj he's passing in to the CUT can either implement ArrayAccess or MyInterface. It looks to me that there's another concept trying to get out. What he should be doing is acepting one Interface/Class in his CUT and allowing that to abstract the difference between the two. Thus get rid of those horrible if...instanceof blocks and be more polymorphic.Trinary
T
5

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);
    }
}
Ticking answered 20/7, 2023 at 6:55 Comment(0)
M
4

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');
Mccabe answered 1/4, 2013 at 20:2 Comment(5)
it is exactly the same thing as creating another interface extending both interfaces. But nastier.Sensorium
My main point was to put it in the test namespace, same file as the test case in which you will use it. You should not put to the app code the interface which you need only for tests that the point. From implementation point of view it ofcource could be an interfaceMccabe
Once again, using a new interface which extends both these interfaces is still the way to go. Your abstract is neither an implementation (even for the tests), and not an interface either. When you're testing a method depending on an interface, you don't have a single care in the world how the thing is implemented. Hence creating an interface extending both interfaces means "Something [here, the mock] is implementing this interface, which also implements both my required interfaces". The message is way clearer than "Something extends an abstract, which asks to implements both interfaces"Sensorium
"using a new interface which extends both these interfaces is still the way to go" Yes, it is better I must admit, but my MAIN point was to put this interface to a test namespace. That's what I was trying to stress on. As long as you use it only in tests of course.Mccabe
The accepted answer is correct, but this is more correct by stressing that such an interface should only be available to your tests.Duffer

© 2022 - 2024 — McMap. All rights reserved.