From 6afda1983f17740e15966465d13007b47dc68fb0 Mon Sep 17 00:00:00 2001 From: Matt Styles Date: Fri, 27 Dec 2019 10:13:30 -0700 Subject: [PATCH] Allow use of Constraints in ReturnValueMap One thing that ReturnValueMap lacked was flexibility regarding the arguments the method was invoked with. It works only with exact match As a side effect, if you have floats as arguments, it may fail. As a solution, support has been added for Framework/Constraint objects. --- .../MockObject/Stub/ReturnValueMap.php | 37 ++++++++- .../Framework/MockObject/MockObjectTest.php | 30 ------- .../MockObject/Stub/ReturnValueMapTest.php | 79 +++++++++++++++++++ 3 files changed, 112 insertions(+), 34 deletions(-) create mode 100644 tests/unit/Framework/MockObject/Stub/ReturnValueMapTest.php diff --git a/src/Framework/MockObject/Stub/ReturnValueMap.php b/src/Framework/MockObject/Stub/ReturnValueMap.php index b44035a70fa..14667851168 100644 --- a/src/Framework/MockObject/Stub/ReturnValueMap.php +++ b/src/Framework/MockObject/Stub/ReturnValueMap.php @@ -9,6 +9,7 @@ */ namespace PHPUnit\Framework\MockObject\Stub; +use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\MockObject\Invocation; /** @@ -28,17 +29,19 @@ public function __construct(array $valueMap) public function invoke(Invocation $invocation) { - $parameterCount = \count($invocation->getParameters()); + $parameters = $invocation->getParameters(); + + $parameterCount = \count($parameters); foreach ($this->valueMap as $map) { if (!\is_array($map) || $parameterCount !== (\count($map) - 1)) { continue; } - $return = \array_pop($map); + $returnValue = $this->getReturnValue(\array_pop($map)); - if ($invocation->getParameters() === $map) { - return $return; + if ($this->compare($parameters, $map)) { + return $returnValue->invoke($invocation); } } } @@ -47,4 +50,30 @@ public function toString(): string { return 'return value from a map'; } + + private function compare(array $actual, array $expected): bool + { + foreach ($expected as $index => $value) { + if ($value instanceof Constraint) { + if ($value->evaluate($actual[$index], '', true) === false) { + return false; + } + } else { + if ($value !== $actual[$index]) { + return false; + } + } + } + + return true; + } + + private function getReturnValue($value): Stub + { + if (!$value instanceof Stub) { + return new ReturnStub($value); + } + + return $value; + } } diff --git a/tests/unit/Framework/MockObject/MockObjectTest.php b/tests/unit/Framework/MockObject/MockObjectTest.php index 732bd16a110..1fbd4c9ccd2 100644 --- a/tests/unit/Framework/MockObject/MockObjectTest.php +++ b/tests/unit/Framework/MockObject/MockObjectTest.php @@ -216,36 +216,6 @@ public function testStubbedReturnValue(): void $this->assertEquals('something', $mock->doSomething()); } - public function testStubbedReturnValueMap(): void - { - $map = [ - ['a', 'b', 'c', 'd'], - ['e', 'f', 'g', 'h'], - ]; - - $mock = $this->getMockBuilder(AnInterface::class) - ->getMock(); - - $mock->expects($this->any()) - ->method('doSomething') - ->will($this->returnValueMap($map)); - - $this->assertEquals('d', $mock->doSomething('a', 'b', 'c')); - $this->assertEquals('h', $mock->doSomething('e', 'f', 'g')); - $this->assertNull($mock->doSomething('foo', 'bar')); - - $mock = $this->getMockBuilder(AnInterface::class) - ->getMock(); - - $mock->expects($this->any()) - ->method('doSomething') - ->willReturnMap($map); - - $this->assertEquals('d', $mock->doSomething('a', 'b', 'c')); - $this->assertEquals('h', $mock->doSomething('e', 'f', 'g')); - $this->assertNull($mock->doSomething('foo', 'bar')); - } - public function testStubbedReturnArgument(): void { $mock = $this->getMockBuilder(AnInterface::class) diff --git a/tests/unit/Framework/MockObject/Stub/ReturnValueMapTest.php b/tests/unit/Framework/MockObject/Stub/ReturnValueMapTest.php new file mode 100644 index 00000000000..da6ccfb0ebe --- /dev/null +++ b/tests/unit/Framework/MockObject/Stub/ReturnValueMapTest.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use PHPUnit\Framework\TestCase; + +/** + * @small + */ +final class ReturnValueMapTest extends TestCase +{ + public function testReturnsTheFirstMatchFound(): void + { + $map = [ + ['a', 'b', 'c', 'd'], + ['a', 'b', 'c', 'e'], + ['e', 'f', 'g', 'h'], + ]; + $mock = $this->getMockBuilder(AnInterface::class) + ->getMock(); + $mock->expects($this->any()) + ->method('doSomething') + ->will($this->returnValueMap($map)); + $this->assertEquals('d', $mock->doSomething('a', 'b', 'c')); + } + + public function testAcceptsFrameworkMatchers(): void + { + $map = [ + [$this->lessThan(2), 1], + [$this->greaterThanOrEqual(2), 2], + ]; + $mock = $this->getMockBuilder(AnInterface::class) + ->getMock(); + $mock->expects($this->any()) + ->method('doSomething') + ->will($this->returnValueMap($map)); + $this->assertEquals(1, $mock->doSomething(0)); + $this->assertEquals(2, $mock->doSomething(100)); + } + + public function testAcceptsStubForReturnValue(): void + { + $callback = $this->returnCallback( + function ($arg) { + return $arg + 1; + } + ); + $map = [ + [$this->lessThan(2), 1], + [$this->greaterThanOrEqual(2), $callback], + ]; + $mock = $this->getMockBuilder(AnInterface::class) + ->getMock(); + $mock->expects($this->any()) + ->method('doSomething') + ->will($this->returnValueMap($map)); + $this->assertEquals(3, $mock->doSomething(2)); + } + + public function testReturnsNullIfNoMatchFound(): void + { + $map = [ + ['a', 'b', 'c', 'd'], + ]; + $mock = $this->getMockBuilder(AnInterface::class) + ->getMock(); + $mock->expects($this->any()) + ->method('doSomething') + ->will($this->returnValueMap($map)); + $this->assertEquals(null, $mock->doSomething('foo', 'bar')); + } +}