From 2efe0edf8e7c621bab310a6260d3cfb57161f5a9 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sat, 28 Oct 2023 09:22:41 +0200 Subject: [PATCH] Closes #5551 --- ChangeLog-10.5.md | 1 + .../MockObject/ConfigurableMethod.php | 36 ++++++++++- .../MockObject/Generator/Generator.php | 8 ++- .../MockObject/Generator/MockMethod.php | 59 ++++++++++++++++++- .../Runtime/Builder/InvocationMocker.php | 43 +++++++++++++- ...ithMethodThatHasDefaultParameterValues.php | 15 +++++ .../MockObject/TestDoubleTestCase.php | 21 +++++++ 7 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 tests/_files/mock-object/InterfaceWithMethodThatHasDefaultParameterValues.php diff --git a/ChangeLog-10.5.md b/ChangeLog-10.5.md index 00b7051d1e2..5d002fd98a6 100644 --- a/ChangeLog-10.5.md +++ b/ChangeLog-10.5.md @@ -7,5 +7,6 @@ All notable changes of the PHPUnit 10.5 release series are documented in this fi ### Added * [#5532](https://github.com/sebastianbergmann/phpunit/issues/5532): `#[IgnoreDeprecations]` attribute to ignore `E_(USER_)DEPRECATED` issues on test class and test method level +* [#5551](https://github.com/sebastianbergmann/phpunit/issues/5551): Support for omitting parameter default values for `willReturnMap()` [10.5.0]: https://github.com/sebastianbergmann/phpunit/compare/10.4...main diff --git a/src/Framework/MockObject/ConfigurableMethod.php b/src/Framework/MockObject/ConfigurableMethod.php index d517ffde4c7..d48d6fa1ee4 100644 --- a/src/Framework/MockObject/ConfigurableMethod.php +++ b/src/Framework/MockObject/ConfigurableMethod.php @@ -20,15 +20,29 @@ final class ConfigurableMethod * @psalm-var non-empty-string */ private readonly string $name; + + /** + * @psalm-var array + */ + private readonly array $defaultParameterValues; + + /** + * @psalm-var non-negative-int + */ + private readonly int $numberOfParameters; private readonly Type $returnType; /** * @psalm-param non-empty-string $name + * @psalm-param array $defaultParameterValues + * @psalm-param non-negative-int $numberOfParameters */ - public function __construct(string $name, Type $returnType) + public function __construct(string $name, array $defaultParameterValues, int $numberOfParameters, Type $returnType) { - $this->name = $name; - $this->returnType = $returnType; + $this->name = $name; + $this->defaultParameterValues = $defaultParameterValues; + $this->numberOfParameters = $numberOfParameters; + $this->returnType = $returnType; } /** @@ -39,6 +53,22 @@ public function name(): string return $this->name; } + /** + * @psalm-return array + */ + public function defaultParameterValues(): array + { + return $this->defaultParameterValues; + } + + /** + * @psalm-return non-negative-int + */ + public function numberOfParameters(): int + { + return $this->numberOfParameters; + } + public function mayReturn(mixed $value): bool { return $this->returnType->isAssignable(Type::fromValue($value, false)); diff --git a/src/Framework/MockObject/Generator/Generator.php b/src/Framework/MockObject/Generator/Generator.php index 45d8460a4fd..c140ca26042 100644 --- a/src/Framework/MockObject/Generator/Generator.php +++ b/src/Framework/MockObject/Generator/Generator.php @@ -730,7 +730,13 @@ private function generateCodeForTestDoubleClass(string $type, bool $mockObject, foreach ($mockMethods->asArray() as $mockMethod) { $mockedMethods .= $mockMethod->generateCode(); - $configurable[] = new ConfigurableMethod($mockMethod->methodName(), $mockMethod->returnType()); + + $configurable[] = new ConfigurableMethod( + $mockMethod->methodName(), + $mockMethod->defaultParameterValues(), + $mockMethod->numberOfParameters(), + $mockMethod->returnType(), + ); } /** @psalm-var trait-string[] $traits */ diff --git a/src/Framework/MockObject/Generator/MockMethod.php b/src/Framework/MockObject/Generator/MockMethod.php index e6955e51bba..4c58e8c6eea 100644 --- a/src/Framework/MockObject/Generator/MockMethod.php +++ b/src/Framework/MockObject/Generator/MockMethod.php @@ -9,6 +9,7 @@ */ namespace PHPUnit\Framework\MockObject\Generator; +use function count; use function explode; use function implode; use function is_object; @@ -54,6 +55,16 @@ final class MockMethod private readonly bool $static; private readonly ?string $deprecation; + /** + * @psalm-var array + */ + private readonly array $defaultParameterValues; + + /** + * @psalm-var non-negative-int + */ + private readonly int $numberOfParameters; + /** * @throws ReflectionException * @throws RuntimeException @@ -94,6 +105,8 @@ public static function fromReflection(ReflectionMethod $method, bool $callOrigin $modifier, self::methodParametersForDeclaration($method), self::methodParametersForCall($method), + self::methodParametersDefaultValues($method), + count($method->getParameters()), (new ReflectionMapper)->fromReturnType($method), $reference, $callOriginalMethod, @@ -115,6 +128,8 @@ public static function fromName(string $className, string $methodName, bool $clo 'public', '', '', + [], + 0, new UnknownType, '', false, @@ -124,10 +139,12 @@ public static function fromName(string $className, string $methodName, bool $clo } /** - * @param class-string $className - * @param non-empty-string $methodName + * @psalm-param class-string $className + * @psalm-param non-empty-string $methodName + * @psalm-param array $defaultParameterValues + * @psalm-param non-negative-int $numberOfParameters */ - private function __construct(string $className, string $methodName, bool $cloneArguments, string $modifier, string $argumentsForDeclaration, string $argumentsForCall, Type $returnType, string $reference, bool $callOriginalMethod, bool $static, ?string $deprecation) + private function __construct(string $className, string $methodName, bool $cloneArguments, string $modifier, string $argumentsForDeclaration, string $argumentsForCall, array $defaultParameterValues, int $numberOfParameters, Type $returnType, string $reference, bool $callOriginalMethod, bool $static, ?string $deprecation) { $this->className = $className; $this->methodName = $methodName; @@ -135,6 +152,8 @@ private function __construct(string $className, string $methodName, bool $cloneA $this->modifier = $modifier; $this->argumentsForDeclaration = $argumentsForDeclaration; $this->argumentsForCall = $argumentsForCall; + $this->defaultParameterValues = $defaultParameterValues; + $this->numberOfParameters = $numberOfParameters; $this->returnType = $returnType; $this->reference = $reference; $this->callOriginalMethod = $callOriginalMethod; @@ -215,6 +234,22 @@ public function returnType(): Type return $this->returnType; } + /** + * @psalm-return array + */ + public function defaultParameterValues(): array + { + return $this->defaultParameterValues; + } + + /** + * @psalm-return non-negative-int + */ + public function numberOfParameters(): int + { + return $this->numberOfParameters; + } + /** * Returns the parameters of a function or method. * @@ -329,4 +364,22 @@ private static function exportDefaultValue(ReflectionParameter $parameter): stri } // @codeCoverageIgnoreEnd } + + /** + * @psalm-return array + */ + private static function methodParametersDefaultValues(ReflectionMethod $method): array + { + $result = []; + + foreach ($method->getParameters() as $i => $parameter) { + if (!$parameter->isDefaultValueAvailable()) { + continue; + } + + $result[$i] = $parameter->getDefaultValue(); + } + + return $result; + } } diff --git a/src/Framework/MockObject/Runtime/Builder/InvocationMocker.php b/src/Framework/MockObject/Runtime/Builder/InvocationMocker.php index f7d1d16863e..fbff6cbdac8 100644 --- a/src/Framework/MockObject/Runtime/Builder/InvocationMocker.php +++ b/src/Framework/MockObject/Runtime/Builder/InvocationMocker.php @@ -13,8 +13,11 @@ use function array_key_exists; use function array_map; use function array_merge; +use function array_pop; +use function assert; use function count; use function is_string; +use function range; use function strtolower; use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\InvalidArgumentException; @@ -117,7 +120,45 @@ public function willReturnReference(mixed &$reference): self public function willReturnMap(array $valueMap): self { - $stub = new ReturnValueMap($valueMap); + $method = $this->configuredMethod(); + + assert($method instanceof ConfigurableMethod); + + $numberOfParameters = $method->numberOfParameters(); + $defaultValues = $method->defaultParameterValues(); + $hasDefaultValues = !empty($defaultValues); + + $_valueMap = []; + + foreach ($valueMap as $mapping) { + $numberOfConfiguredParameters = count($mapping) - 1; + + if ($numberOfConfiguredParameters === $numberOfParameters || !$hasDefaultValues) { + $_valueMap[] = $mapping; + + continue; + } + + $_mapping = []; + $returnValue = array_pop($mapping); + + foreach (range(0, $numberOfParameters - 1) as $i) { + if (isset($mapping[$i])) { + $_mapping[] = $mapping[$i]; + + continue; + } + + if (isset($defaultValues[$i])) { + $_mapping[] = $defaultValues[$i]; + } + } + + $_mapping[] = $returnValue; + $_valueMap[] = $_mapping; + } + + $stub = new ReturnValueMap($_valueMap); return $this->will($stub); } diff --git a/tests/_files/mock-object/InterfaceWithMethodThatHasDefaultParameterValues.php b/tests/_files/mock-object/InterfaceWithMethodThatHasDefaultParameterValues.php new file mode 100644 index 00000000000..7b10933e685 --- /dev/null +++ b/tests/_files/mock-object/InterfaceWithMethodThatHasDefaultParameterValues.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture\MockObject; + +interface InterfaceWithMethodThatHasDefaultParameterValues +{ + public function doSomething(int $a, int $b = 1): int; +} diff --git a/tests/unit/Framework/MockObject/TestDoubleTestCase.php b/tests/unit/Framework/MockObject/TestDoubleTestCase.php index 1c3022e64db..f2cda235e0f 100644 --- a/tests/unit/Framework/MockObject/TestDoubleTestCase.php +++ b/tests/unit/Framework/MockObject/TestDoubleTestCase.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use PHPUnit\TestFixture\MockObject\ExtendableClassWithCloneMethod; use PHPUnit\TestFixture\MockObject\InterfaceWithMethodThatExpectsObject; +use PHPUnit\TestFixture\MockObject\InterfaceWithMethodThatHasDefaultParameterValues; use PHPUnit\TestFixture\MockObject\InterfaceWithNeverReturningMethod; use PHPUnit\TestFixture\MockObject\InterfaceWithReturnTypeDeclaration; use stdClass; @@ -137,6 +138,26 @@ final public function testMethodCanBeConfiguredToReturnValuesBasedOnArgumentMapp $this->assertSame(4, $double->doSomethingElse(3)); } + final public function testMethodWithDefaultParameterValuesCanBeConfiguredToReturnValuesBasedOnArgumentMapping(): void + { + $double = $this->createTestDouble(InterfaceWithMethodThatHasDefaultParameterValues::class); + + $double->method('doSomething')->willReturnMap([[1, 2, 3], [4, 5, 6]]); + + $this->assertSame(3, $double->doSomething(1, 2)); + $this->assertSame(6, $double->doSomething(4, 5)); + } + + final public function testMethodWithDefaultParameterValuesCanBeConfiguredToReturnValuesBasedOnArgumentMappingThatOmitsDefaultValues(): void + { + $double = $this->createTestDouble(InterfaceWithMethodThatHasDefaultParameterValues::class); + + $double->method('doSomething')->willReturnMap([[1, 2], [3, 4]]); + + $this->assertSame(2, $double->doSomething(1)); + $this->assertSame(4, $double->doSomething(3)); + } + final public function testMethodCanBeConfiguredToReturnValuesUsingCallback(): void { $double = $this->createTestDouble(InterfaceWithReturnTypeDeclaration::class);