From 1ce8bfbc960c4b71536e9df5ae35d08a2de556f5 Mon Sep 17 00:00:00 2001
From: Jaapio <jaap@ijaap.nl>
Date: Mon, 1 Jun 2020 10:14:39 +0200
Subject: [PATCH] Throw exception on non unique mocked method

This patch will make a mock throw an exception when multiple matchers
can be applied to a invoke. When allowing this, results of tests are
not predictable.

refs #4255
---
 .../MockObject/InvocationHandler.php          | 47 ++++++++++---------
 .../Builder/InvocationMockerTest.php          | 23 +++++++++
 2 files changed, 48 insertions(+), 22 deletions(-)

diff --git a/src/Framework/MockObject/InvocationHandler.php b/src/Framework/MockObject/InvocationHandler.php
index 7b2b2bb7493..5c5ddb712e8 100644
--- a/src/Framework/MockObject/InvocationHandler.php
+++ b/src/Framework/MockObject/InvocationHandler.php
@@ -113,30 +113,11 @@ public function expects(InvocationOrder $rule): InvocationMocker
     public function invoke(Invocation $invocation)
     {
         $exception      = null;
-        $hasReturnValue = false;
         $returnValue    = null;
+        $match          = $this->findMatcher($invocation);
 
-        foreach ($this->matchers as $match) {
-            try {
-                if ($match->matches($invocation)) {
-                    $value = $match->invoked($invocation);
-
-                    if (!$hasReturnValue) {
-                        $returnValue    = $value;
-                        $hasReturnValue = true;
-                    }
-                }
-            } catch (\Exception $e) {
-                $exception = $e;
-            }
-        }
-
-        if ($exception !== null) {
-            throw $exception;
-        }
-
-        if ($hasReturnValue) {
-            return $returnValue;
+        if ($match !== null) {
+            return $match->invoked($invocation);
         }
 
         if (!$this->returnValueGeneration) {
@@ -160,6 +141,28 @@ public function invoke(Invocation $invocation)
         return $invocation->generateReturnValue();
     }
 
+    private function findMatcher(Invocation $invocation): ?Matcher
+    {
+        $result = [];
+        foreach ($this->matchers as $matcher) {
+            if ($matcher->matches($invocation)) {
+                $result[] = $matcher;
+            }
+        }
+
+        if (count($result) > 1) {
+            throw new ExpectationFailedException(
+                sprintf(
+                    'Non unique mocked method invocation: %s::%s',
+                    $invocation->getClassName(),
+                    $invocation->getMethodName()
+                )
+            );
+        }
+
+        return current($result) ?: null;
+    }
+
     public function matches(Invocation $invocation): bool
     {
         foreach ($this->matchers as $matcher) {
diff --git a/tests/unit/Framework/MockObject/Builder/InvocationMockerTest.php b/tests/unit/Framework/MockObject/Builder/InvocationMockerTest.php
index 429e35318c2..7d334e0fd1e 100644
--- a/tests/unit/Framework/MockObject/Builder/InvocationMockerTest.php
+++ b/tests/unit/Framework/MockObject/Builder/InvocationMockerTest.php
@@ -9,6 +9,7 @@
  * file that was distributed with this source code.
  */
 use PHPUnit\Framework\Constraint\IsEqual;
+use PHPUnit\Framework\ExpectationFailedException;
 use PHPUnit\Framework\MockObject\Builder\InvocationMocker;
 use PHPUnit\Framework\MockObject\IncompatibleReturnValueException;
 use PHPUnit\Framework\MockObject\InvocationHandler;
@@ -257,4 +258,26 @@ public function testWillReturnAlreadyInstantiatedStubs(): void
         $this->assertSame('foo', $mock->foo());
         $this->assertSame($mock, $mock->bar());
     }
+
+    public function testMultipleWithParametersWillReturnLatestDefined(): void
+    {
+        $mock = $this->getMockBuilder(stdClass::class)
+            ->setMethods(['foo'])
+            ->getMock();
+
+        $mock->expects($this->any())
+            ->method('foo')
+            ->with('bar')
+            ->willReturn('first');
+
+        $mock->expects($this->any())
+            ->method('foo')
+            ->with('foo')
+            ->willReturn('second');
+
+       $this->expectException(ExpectationFailedException::class);
+       $this->getExpectedExceptionMessage('Non unique mocked method invocation: stdClass::foo');
+
+        $mock->foo('bar');
+    }
 }