sebastianbergmann / phpunit

The PHP Unit Testing framework.
https://phpunit.de/
BSD 3-Clause "New" or "Revised" License
19.66k stars 2.2k forks source link

Mocking same method with different "with" and different "return" throw an exception #4398

Open oleg-andreyev opened 4 years ago

oleg-andreyev commented 4 years ago
Q A
PHPUnit version 9.2.6
PHP version 7.4.8
Installation Method Composer

Summary

Mocking same method with different "with" and different "return"

Current behavior

Expectation failed for method name is "getRepository" when invoked zero or more times
Parameter 0 for invocation Doctrine\ORM\EntityManager::getRepository('Op\AppBundle\Entity\Order') does not match expected value.
Failed asserting that two strings are equal.
Expected :'Op\AppBundle\Entity\Customer'
Actual   :'Op\AppBundle\Entity\Order'

How to reproduce

        $this->em
            ->method('getRepository')
            ->with(Customer::class)
            ->willReturn($this->customerRepository);

        $this->em
            ->method('getRepository')
            ->with(Order::class)
            ->willReturn($this->orderRepository);
$this->em->getRepository(Order::class)->findOneByOrderId($id);

image

Expected behavior

If "matcher" did not throw an exception on the second "match" (invoke), we should not throw an exception

sebastianbergmann commented 3 years ago

Thank you for your report.

Please provide a minimal, self-contained, reproducing test case that shows the problem you are reporting.

Without such a minimal, self-contained, reproducing test case I will not be able to investigate this issue.

emdadulsadik commented 1 year ago

@sebastianbergmann , I have sucessfully reproduced the issue (PHPUnit 9.6.3 by Sebastian Bergmann and contributors.) To reproduce you can use TestCase and mock the doctrine's entity manager, have the entity manager match the getRepository method with two repository classes consecutively (order does not matter); you will see the matcher exception. Example:


// doctrine entities
class Order {}
class Customer {}

// doctrine repositories
class OrderRepository {}
class CustomerRepository {}

// Test Subject

class Order {

        public function __construct(EntityManagerInterface $em) {
             $order = $this->$em->getRepository(OrderRepository::class)->findBy(['id' => 1]);
        }
}

class OrderTest extends TestCase
{

        protected function setUp(): void {
               $this->em = $this->createMock(EntityManagerInterface::class);
               $this->customerRepositoryMock = $this->createMock(CustomerRepository::class);
               $this->orderRepositoryMock = $this->createMock(OrderRepository::class);  

               $this->em->method('getRepository')->with(Customer::class)
                       ->willReturn($this->customerRepositoryMock);

               $this->em->method('getRepository')->with(Order::class)
                       ->willReturn($this->orderRepositoryMock);
       }
}

// Then try test a class that uses these mock and try to get anything from these repositories,
// it will throw. 
// I hope I could make it reproducible for you. 
oleg-andreyev commented 1 year ago

Thank you for your report.

Please provide a minimal, self-contained, reproducing test case that shows the problem you are reporting.

Without such a minimal, self-contained, reproducing test case I will not be able to investigate this issue.

Lol. It's been 2y+, somehow I've missed this notification. It's irrelevant for me atm. 💪👍

sebastianbergmann commented 1 year ago

Sorry, but something that uses Doctrine's entity manager is neither minimal nor self-contained.

kubawerlos commented 1 week ago

Latest 10.5 branch:

kub@:~/code/sebastianbergmann/phpunit(10.5)$ cat test.php
<?php

interface I { public function get(string $x): int; }
class C1 {}
class C2 {}

class Test extends PHPUnit\Framework\TestCase
{
    private $mock;

    protected function setUp(): void
    {
        $this->mock = $this->createMock(I::class);
        $this->mock->method('get')->with(C1::class)
            ->willReturn(1);
        $this->mock->method('get')->with(C2::class)
            ->willReturn(2);
    }

    public function testSomething(): void
    {
        // 1 of the 2 lines below can be commented out and exception is still thrown
        $this->mock->get(C1::class);
        $this->mock->get(C2::class);

        $this->assertTrue(true);
    }
}

kub@:~/code/sebastianbergmann/phpunit(10.5)$ php phpunit test.php
PHPUnit 10.5.32-4-g8d77a5c4e by Sebastian Bergmann and contributors.

Runtime:       PHP 8.3.11
Configuration: /home/kuba/code/sebastianbergmann/phpunit/phpunit.xml

F                                                                   1 / 1 (100%)

Time: 00:00.005, Memory: 8.00 MB

There was 1 failure:

1) Test::testSomething
Expectation failed for method name is "get" when invoked zero or more times
Parameter 0 for invocation I::get('C1'): int does not match expected value.
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'C2'
+'C1'

/home/kuba/code/sebastianbergmann/phpunit/src/Framework/MockObject/Runtime/Matcher.php:110
/home/kuba/code/sebastianbergmann/phpunit/src/Framework/MockObject/Runtime/InvocationHandler.php:109
/home/kuba/code/sebastianbergmann/phpunit/test.php:23
/home/kuba/code/sebastianbergmann/phpunit/src/Framework/TestCase.php:1188
/home/kuba/code/sebastianbergmann/phpunit/src/Framework/TestCase.php:687
/home/kuba/code/sebastianbergmann/phpunit/src/Framework/TestRunner.php:106
/home/kuba/code/sebastianbergmann/phpunit/src/Framework/TestCase.php:517
/home/kuba/code/sebastianbergmann/phpunit/src/Framework/TestSuite.php:380
/home/kuba/code/sebastianbergmann/phpunit/src/TextUI/TestRunner.php:64
/home/kuba/code/sebastianbergmann/phpunit/src/TextUI/Application.php:202

--

There was 1 risky test:

1) Test::testSomething
This test did not perform any assertions

/home/kuba/code/sebastianbergmann/phpunit/test.php:20

FAILURES!
Tests: 1, Assertions: 0, Failures: 1, Risky: 1.

Isn't the:

        $this->mock->method('get')->with(C1::class)
            ->willReturn(1);
        $this->mock->method('get')->with(C2::class)
            ->willReturn(2);

incorrect way of mocking method and should be replaced with:

        $this->mock->method('get')->willReturnCallback(fn (string $x) => match (true) {
            $x === C1::class => 1,
            $x === C2::class => 2,
            default => throw new LogicException(),
        });

I wonder?