diff --git a/src/Factories/Attribute.php b/src/Factories/Attribute.php index 21147340..1f2a4e93 100644 --- a/src/Factories/Attribute.php +++ b/src/Factories/Attribute.php @@ -10,7 +10,7 @@ final class Attribute { /** - * @param iterable $arguments + * @param iterable $arguments */ public function __construct(public string $name, public iterable $arguments) { diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 1b02ed1e..34cea90b 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -513,28 +513,51 @@ public function note(array|string $note): self /** * Sets the covered classes or methods. * - * @param array|string $classesOrFunctions + * @param array>|string|array ...$classesOrFunctions */ public function covers(array|string ...$classesOrFunctions): self { - /** @var array $classesOrFunctions */ - $classesOrFunctions = array_reduce($classesOrFunctions, fn ($carry, $item): array => is_array($item) ? array_merge($carry, $item) : array_merge($carry, [$item]), []); // @pest-ignore-type + /** @var array>|array $classesOrFunctions */ + $classesOrFunctions = array_reduce($classesOrFunctions, function (array $carry, string|array $item): array { + if (is_array($item) && (count($item) !== 2 || ! is_string($item[0]) || ! is_string($item[1]) || ! \method_exists($item[0], $item[1]))) { + return array_merge($carry, $item); + } - foreach ($classesOrFunctions as $classOrFunction) { - $isClass = class_exists($classOrFunction) || interface_exists($classOrFunction) || enum_exists($classOrFunction); - $isTrait = trait_exists($classOrFunction); - $isFunction = function_exists($classOrFunction); + return array_merge($carry, [$item]); + }, []); + + if (count($classesOrFunctions) === 2 && is_string($classesOrFunctions[0]) && is_string($classesOrFunctions[1]) && \method_exists($classesOrFunctions[0], $classesOrFunctions[1])) { + $classesOrFunctions = [$classesOrFunctions]; + } - if (! $isClass && ! $isTrait && ! $isFunction) { - throw new InvalidArgumentException(sprintf('No class, trait or method named "%s" has been found.', $classOrFunction)); + foreach ($classesOrFunctions as $classOrFunction) { + /** @var string|array $classOrFunction */ + $isClassMethod = is_array($classOrFunction) && method_exists($classOrFunction[0], $classOrFunction[1]) && class_exists($classOrFunction[0]); + $isClass = ! is_array($classOrFunction) && (class_exists($classOrFunction) || interface_exists($classOrFunction) || enum_exists($classOrFunction)); + $isTrait = ! is_array($classOrFunction) && trait_exists($classOrFunction); + $isFunction = ! is_array($classOrFunction) && function_exists($classOrFunction); + + if (! $isClass && ! $isTrait && ! $isFunction && ! $isClassMethod) { + throw new InvalidArgumentException( + sprintf( + 'No class, method, trait or function named "%s" has been found.', + is_array($classOrFunction) ? $classOrFunction[0].'::'.$classOrFunction[1] : $classOrFunction + ) + ); } if ($isClass) { + /** @var string $classOrFunction */ $this->coversClass($classOrFunction); } elseif ($isTrait) { + /** @var string $classOrFunction */ $this->coversTrait($classOrFunction); - } else { + } elseif ($isFunction) { + /** @var string $classOrFunction */ $this->coversFunction($classOrFunction); + } elseif ($isClassMethod) { + /** @var array $classOrFunction */ + $this->coversMethod($classOrFunction); } } @@ -587,6 +610,23 @@ public function coversTrait(string ...$traits): self return $this; } + /** + * Sets the covered methods. + * + * @param array ...$methods + */ + public function coversMethod(array ...$methods): self + { + foreach ($methods as $method) { + $this->testCaseFactoryAttributes[] = new Attribute( + \PHPUnit\Framework\Attributes\CoversMethod::class, + $method, + ); + } + + return $this; + } + /** * Sets the covered functions. */ diff --git a/tests/Features/Covers.php b/tests/Features/Covers.php index 386d523f..80d981ae 100644 --- a/tests/Features/Covers.php +++ b/tests/Features/Covers.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversFunction; use Tests\Fixtures\Covers\CoversClass1; +use Tests\Fixtures\Covers\CoversClass2; use Tests\Fixtures\Covers\CoversClass3; use Tests\Fixtures\Covers\CoversTrait; @@ -53,7 +54,51 @@ function testCoversFunction() {} })->coversNothing(); it('throws exception if no class nor method has been found', function () { - $testCall = new TestCall(TestSuite::getInstance(), 'filename', 'description', fn () => 'closure'); + $testCall = new TestCall(TestSuite::getInstance(), 'filename', 'no class nor method has been found', fn () => 'closure'); $testCall->covers('fakeName'); -})->throws(InvalidArgumentException::class, 'No class, trait or method named "fakeName" has been found.'); +})->throws(InvalidArgumentException::class, 'No class, method, trait or function named "fakeName" has been found.'); + +it('uses the correct PHPUnit attribute for covers with single class method as array', function () { + $attributes = (new ReflectionClass($this))->getAttributes(); + + expect($attributes[12]->getName())->toBe('PHPUnit\Framework\Attributes\CoversMethod'); + expect($attributes[12]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1'); + expect($attributes[12]->getArguments()[1])->toBe('foo'); +})->covers([[CoversClass1::class, 'foo']]); + +it('uses the correct PHPUnit attribute for covers with single class method', function () { + $attributes = (new ReflectionClass($this))->getAttributes(); + + expect($attributes[14]->getName())->toBe('PHPUnit\Framework\Attributes\CoversMethod'); + expect($attributes[14]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1'); + expect($attributes[14]->getArguments()[1])->toBe('foo'); +})->covers([CoversClass1::class, 'foo']); + +it('uses the correct PHPUnit attribute for mixed covers with class method', function () { + $attributes = (new ReflectionClass($this))->getAttributes(); + + expect($attributes[16]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass'); + expect($attributes[16]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass2'); + + expect($attributes[17]->getName())->toBe('PHPUnit\Framework\Attributes\CoversMethod'); + expect($attributes[17]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1'); + expect($attributes[17]->getArguments()[1])->toBe('foo'); +})->covers(CoversClass2::class, [CoversClass1::class, 'foo']); + +it('uses the correct PHPUnit attribute for mixed covers with class method as array', function () { + $attributes = (new ReflectionClass($this))->getAttributes(); + + expect($attributes[19]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass'); + expect($attributes[19]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass2'); + + expect($attributes[20]->getName())->toBe('PHPUnit\Framework\Attributes\CoversMethod'); + expect($attributes[20]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1'); + expect($attributes[20]->getArguments()[1])->toBe('foo'); +})->covers([CoversClass2::class, [CoversClass1::class, 'foo']]); + +it('throws exception if no class method has been found', function () { + $testCall = new TestCall(TestSuite::getInstance(), 'filename', 'no class method has been found', fn () => 'closure'); + + $testCall->covers([['fakeClass', 'fakeMethod']]); +})->throws(InvalidArgumentException::class, 'No class, method, trait or function named "fakeClass::fakeMethod" has been found.');