-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Open
Labels
feature/test-doublesTest Stubs and Mock ObjectsTest Stubs and Mock Objectstype/enhancementA new idea that should be implementedA new idea that should be implemented
Description
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.
laloona
Metadata
Metadata
Assignees
Labels
feature/test-doublesTest Stubs and Mock ObjectsTest Stubs and Mock Objectstype/enhancementA new idea that should be implementedA new idea that should be implemented