diff --git a/src/Exceptions/DatasetMissing.php b/src/Exceptions/DatasetMissing.php new file mode 100644 index 00000000..b8f0cb2d --- /dev/null +++ b/src/Exceptions/DatasetMissing.php @@ -0,0 +1,36 @@ + $args A map of argument names to their typee + */ + public function __construct(string $file, string $name, array $args) + { + parent::__construct(sprintf( + "A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", + $name, + count($args), + implode(', ', array_map(static function (string $arg, string $type): string { + return sprintf('%s $%s', $type, $arg); + }, array_keys($args), $args)), + $file, + )); + } +} diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 6efe63f4..bc75f5c1 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -228,4 +228,13 @@ final class TestCaseFactory return $classFQN; } + + /** + * Determine if the test case will receive argument input from Pest, or not. + */ + public function receivesArguments(): bool + { + return count($this->datasets) > 0 + || $this->factoryProxies->count('addDependencies') > 0; + } } diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index b2eb4893..47684548 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -5,11 +5,13 @@ declare(strict_types=1); namespace Pest\Repositories; use Closure; +use Pest\Exceptions\DatasetMissing; use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\TestAlreadyExist; use Pest\Exceptions\TestCaseAlreadyInUse; use Pest\Exceptions\TestCaseClassOrTraitNotFound; use Pest\Factories\TestCaseFactory; +use Pest\Support\Reflection; use Pest\Support\Str; use Pest\TestSuite; use PHPUnit\Framework\TestCase; @@ -140,6 +142,14 @@ final class TestRepository throw new TestAlreadyExist($test->filename, $test->description); } + if (!$test->receivesArguments()) { + $arguments = Reflection::getFunctionArguments($test->test); + + if (count($arguments) > 0) { + throw new DatasetMissing($test->filename, $test->description, $arguments); + } + } + $this->state[sprintf('%s%s%s', $test->filename, self::SEPARATOR, $test->description)] = $test; } } diff --git a/src/Support/HigherOrderMessageCollection.php b/src/Support/HigherOrderMessageCollection.php index b107bdba..a6634685 100644 --- a/src/Support/HigherOrderMessageCollection.php +++ b/src/Support/HigherOrderMessageCollection.php @@ -53,4 +53,20 @@ final class HigherOrderMessageCollection $message->call($target); } } + + /** + * Count the number of messages with the given name. + * + * @param string $name A higher order message name (usually a method name) + */ + public function count(string $name): int + { + return array_reduce( + $this->messages, + static function (int $total, HigherOrderMessage $message) use ($name): int { + return $total + (int) ($name === $message->name); + }, + 0, + ); + } } diff --git a/src/Support/Reflection.php b/src/Support/Reflection.php index 819ad4a5..44cd754c 100644 --- a/src/Support/Reflection.php +++ b/src/Support/Reflection.php @@ -12,6 +12,7 @@ use ReflectionException; use ReflectionFunction; use ReflectionNamedType; use ReflectionParameter; +use ReflectionUnionType; /** * @internal @@ -170,4 +171,37 @@ final class Reflection return $name; } + + /** + * Receive a map of function argument names to their types. + * + * @return array + */ + public static function getFunctionArguments(Closure $function): array + { + $parameters = (new ReflectionFunction($function))->getParameters(); + $arguments = []; + + foreach ($parameters as $parameter) { + /** @var ReflectionNamedType|ReflectionUnionType|null $types */ + $types = ($parameter->hasType()) ? $parameter->getType() : null; + + if (is_null($types)) { + $arguments[$parameter->getName()] = 'mixed'; + + continue; + } + + $arguments[$parameter->getName()] = implode('|', array_map( + static function (ReflectionNamedType $type): string { + return $type->getName(); + }, + ($types instanceof ReflectionNamedType) + ? [$types] // NOTE: normalize as list of to handle unions + : $types->getTypes(), + )); + } + + return $arguments; + } } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 74ededc0..0a23a6e9 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -573,6 +573,7 @@ PASS Tests\Unit\TestSuite ✓ it does not allow to add the same test description twice + ✓ it alerts users about tests with arguments but no input PASS Tests\Visual\Help ✓ visual snapshot of help command output @@ -600,5 +601,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 380 passed + Tests: 4 incompleted, 9 skipped, 381 passed \ No newline at end of file diff --git a/tests/Unit/TestSuite.php b/tests/Unit/TestSuite.php index 74d9c092..62cc724d 100644 --- a/tests/Unit/TestSuite.php +++ b/tests/Unit/TestSuite.php @@ -1,5 +1,6 @@ tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test)); - $this->expectException(TestAlreadyExist::class); - $this->expectExceptionMessage(sprintf('A test with the description `%s` already exist in the filename `%s`.', 'foo', __FILE__)); $testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test)); -}); +})->throws( + TestAlreadyExist::class, + sprintf('A test with the description `%s` already exist in the filename `%s`.', 'foo', __FILE__), +); + +it('alerts users about tests with arguments but no input', function () { + $testSuite = new TestSuite(getcwd(), 'tests'); + $test = function (int $arg) {}; + $testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test)); +})->throws( + DatasetMissing::class, + sprintf("A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", 'foo', 1, 'int $arg', __FILE__), +);