Skip to content

Expect calls to the same method of a mock object but with different argumentsΒ #6407

@sebastianbergmann

Description

@sebastianbergmann

It should be possible to expect calls to the same method of a mock object but with different arguments.

Consider this code:

src/Event.php

<?php declare(strict_types=1);
namespace PHPUnit\TestFixture\Issue6406;

interface Event
{
}

src/AnEvent.php

<?php declare(strict_types=1);
namespace PHPUnit\TestFixture\Issue6406;

final readonly class AnEvent implements Event
{
}

src/AnotherEvent.php

<?php declare(strict_types=1);
namespace PHPUnit\TestFixture\Issue6406;

final readonly class AnotherEvent implements Event
{
}

src/Dispatcher.php

<?php declare(strict_types=1);
namespace PHPUnit\TestFixture\Issue6406;

interface Dispatcher
{
    public function dispatch(Event $event): void;
}

src/Service.php

final readonly class Service
{
    private Dispatcher $dispatcher;

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

    public function doSomething(): void
    {
        $this->dispatcher->dispatch(new AnEvent);
        $this->dispatcher->dispatch(new AnotherEvent);
    }
}

In a test for Service we currently cannot (conveniently) configure that we expect two calls of the Dispatcher::dispatch() method, once with an instance of AnEvent and once with an instance of AnotherEvent:

tests/ServiceTest.php

<?php declare(strict_types=1);
namespace PHPUnit\TestFixture\Issue6406;

use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;

interface Event
{
}

final readonly class AnEvent implements Event
{
}

final readonly class AnotherEvent implements Event
{
}

interface Dispatcher
{
    public function dispatch(Event $event): void;
}

final readonly class Service
{
    private Dispatcher $dispatcher;

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

    public function doSomething(): void
    {
        $this->dispatcher->dispatch(new AnEvent);
        $this->dispatcher->dispatch(new AnotherEvent);
    }
}

final class ServiceTest extends TestCase
{
    public function testDoesNotWork(): void
    {
        $dispatcher = $this->createMock(Dispatcher::class);

        $dispatcher
            ->expects($this->exactly(2))
            ->method('dispatch')
            ->with($this->isInstanceOf(AnEvent::class))
            ->with($this->isInstanceOf(AnotherEvent::class));

        $service = new Service($dispatcher);

        $service->doSomething();
    }

    public function testAlsoDoesNotWork(): void
    {
        $dispatcher = $this->createMock(Dispatcher::class);

        $dispatcher
            ->expects($this->once())
            ->method('dispatch')
            ->with($this->isInstanceOf(AnEvent::class));

        $dispatcher
            ->expects($this->once())
            ->method('dispatch')
            ->with($this->isInstanceOf(AnotherEvent::class));

        $service = new Service($dispatcher);

        $service->doSomething();
    }

    public function testWorksButIsNotSufficient(): void
    {
        $dispatcher = $this->createMock(Dispatcher::class);

        $dispatcher
            ->expects($this->exactly(2))
            ->method('dispatch')
            ->with($this->logicalOr(
                $this->isInstanceOf(AnEvent::class),
                $this->isInstanceOf(AnotherEvent::class),
            ));

        $service = new Service($dispatcher);

        $service->doSomething();
    }

    public function testWorksButIsInconvenient(): void
    {
        $dispatcher = $this->createMock(Dispatcher::class);

        $dispatcher
            ->expects($this->exactly(2))
            ->method('dispatch')
            ->with($this->callback(
                new class
                {
                    private const EXPECTED = [AnEvent::class, AnotherEvent::class];
                    private int $invocation = 0;

                    public function __invoke(Event $event): bool
                    {
                        return $event::class === self::EXPECTED[$this->invocation++];
                    }
                }
            ));

        $service = new Service($dispatcher);

        $service->doSomething();
    }
}

Running the test shown above yields the output shown below:

PHPUnit 13.0-g18f2df6f02 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.4.14

EF..                                                                4 / 4 (100%)

Time: 00:00.003, Memory: 6.00 MB

There was 1 error:

1) PHPUnit\TestFixture\Issue6406\ServiceTest::testDoesNotWork
PHPUnit\Framework\MockObject\MethodParametersAlreadyConfiguredException: Method parameters already configured

/usr/local/src/phpunit/src/Framework/MockObject/Runtime/InvocationStubberImplementation.php:287
/usr/local/src/phpunit/src/Framework/MockObject/Runtime/InvocationStubberImplementation.php:135
/usr/local/src/phpunit/Test.php:50
/usr/local/src/phpunit/src/Framework/TestCase.php:1316
/usr/local/src/phpunit/src/Framework/TestCase.php:517
/usr/local/src/phpunit/src/Framework/TestRunner/TestRunner.php:99
/usr/local/src/phpunit/src/Framework/TestCase.php:358
/usr/local/src/phpunit/src/Framework/TestSuite.php:374
/usr/local/src/phpunit/src/TextUI/TestRunner.php:64
/usr/local/src/phpunit/src/TextUI/Application.php:229

--

There was 1 failure:

1) PHPUnit\TestFixture\Issue6406\ServiceTest::testAlsoDoesNotWork
Expectation failed for method name is "dispatch" when invoked 1 time
Parameter 0 for invocation PHPUnit\TestFixture\Issue6406\Dispatcher::dispatch(PHPUnit\TestFixture\Issue6406\AnEvent Object ()): void does not match expected value.
Failed asserting that an instance of class PHPUnit\TestFixture\Issue6406\AnEvent is an instance of class PHPUnit\TestFixture\Issue6406\AnotherEvent.

/usr/local/src/phpunit/src/Framework/MockObject/Runtime/Matcher.php:117
/usr/local/src/phpunit/src/Framework/MockObject/Runtime/InvocationHandler.php:110
/usr/local/src/phpunit/Test.php:35
/usr/local/src/phpunit/Test.php:73
/usr/local/src/phpunit/src/Framework/TestCase.php:1316
/usr/local/src/phpunit/src/Framework/TestCase.php:517
/usr/local/src/phpunit/src/Framework/TestRunner/TestRunner.php:99
/usr/local/src/phpunit/src/Framework/TestCase.php:358
/usr/local/src/phpunit/src/Framework/TestSuite.php:374
/usr/local/src/phpunit/src/TextUI/TestRunner.php:64
/usr/local/src/phpunit/src/TextUI/Application.php:229

ERRORS!
Tests: 4, Assertions: 8, Errors: 1, Failures: 1.

Metadata

Metadata

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions