testCaseMethod = new TestCaseMethodFactory($filename, $description, $closure); $this->descriptionLess = $description === null; $this->describing = DescribeCall::describing(); $this->testSuite->beforeEach->get($this->filename)[0]($this); } /** * Asserts that the test throws the given `$exceptionClass` when called. */ public function throws(string|int $exception, ?string $exceptionMessage = null, ?int $exceptionCode = null): self { if (is_int($exception)) { $exceptionCode = $exception; } elseif (class_exists($exception)) { $this->testCaseMethod ->proxies ->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exception]); } else { $exceptionMessage = $exception; } if (is_string($exceptionMessage)) { $this->testCaseMethod ->proxies ->add(Backtrace::file(), Backtrace::line(), 'expectExceptionMessage', [$exceptionMessage]); } if (is_int($exceptionCode)) { $this->testCaseMethod ->proxies ->add(Backtrace::file(), Backtrace::line(), 'expectExceptionCode', [$exceptionCode]); } return $this; } /** * Asserts that the test throws the given `$exceptionClass` when called if the given condition is true. * * @param (callable(): bool)|bool $condition */ public function throwsIf(callable|bool $condition, string|int $exception, ?string $exceptionMessage = null, ?int $exceptionCode = null): self { $condition = is_callable($condition) ? $condition : static fn (): bool => $condition; if ($condition()) { return $this->throws($exception, $exceptionMessage, $exceptionCode); } return $this; } /** * Asserts that the test throws the given `$exceptionClass` when called if the given condition is false. * * @param (callable(): bool)|bool $condition */ public function throwsUnless(callable|bool $condition, string|int $exception, ?string $exceptionMessage = null, ?int $exceptionCode = null): self { $condition = is_callable($condition) ? $condition : static fn (): bool => $condition; if (! $condition()) { return $this->throws($exception, $exceptionMessage, $exceptionCode); } return $this; } /** * Runs the current test multiple times with * each item of the given `iterable`. * * @param array<\Closure|iterable|string> $data */ public function with(Closure|iterable|string ...$data): self { foreach ($data as $dataset) { $this->testCaseMethod->datasets[] = $dataset; } return $this; } /** * Sets the test depends. */ public function depends(string ...$depends): self { foreach ($depends as $depend) { $this->testCaseMethod->depends[] = $depend; } return $this; } /** * Sets the test group(s). */ public function group(string ...$groups): self { foreach ($groups as $group) { $this->testCaseMethod->groups[] = $group; } return $this; } /** * Filters the test suite by "only" tests. */ public function only(): self { Only::enable($this); return $this; } /** * Skips the current test. */ public function skip(Closure|bool|string $conditionOrMessage = true, string $message = ''): self { $condition = is_string($conditionOrMessage) ? NullClosure::create() : $conditionOrMessage; $condition = is_callable($condition) ? $condition : fn (): bool => $condition; $message = is_string($conditionOrMessage) ? $conditionOrMessage : $message; /** @var callable(): bool $condition */ $condition = $condition->bindTo(null); $this->testCaseMethod ->chains ->addWhen($condition, $this->filename, Backtrace::line(), 'markTestSkipped', [$message]); return $this; } /** * Skips the current test if the given test is running on Windows. */ public function skipOnWindows(): self { return $this->skipOn('Windows', 'This test is skipped on [Windows].'); } /** * Skips the current test if the given test is running on Mac OS. */ public function skipOnMac(): self { return $this->skipOn('Darwin', 'This test is skipped on [Mac].'); } /** * Skips the current test if the given test is running on Linux. */ public function skipOnLinux(): self { return $this->skipOn('Linux', 'This test is skipped on [Linux].'); } /** * Skips the current test if the given test is running on the given operating systems. */ private function skipOn(string $osFamily, string $message): self { return $osFamily === PHP_OS_FAMILY ? $this->skip($message) : $this; } /** * Skips the current test unless the given test is running on Windows. */ public function onlyOnWindows(): self { return $this->skipOnMac()->skipOnLinux(); } /** * Skips the current test unless the given test is running on Mac. */ public function onlyOnMac(): self { return $this->skipOnWindows()->skipOnLinux(); } /** * Skips the current test unless the given test is running on Linux. */ public function onlyOnLinux(): self { return $this->skipOnWindows()->skipOnMac(); } /** * Repeats the current test the given number of times. */ public function repeat(int $times): self { if ($times < 1) { throw new InvalidArgumentException('The number of repetitions must be greater than 0.'); } $this->testCaseMethod->repetitions = $times; return $this; } /** * Sets the test as "todo". */ public function todo(): self { $this->skip('__TODO__'); $this->testCaseMethod->todo = true; return $this; } /** * Sets the covered classes or methods. */ public function covers(string ...$classesOrFunctions): self { foreach ($classesOrFunctions as $classOrFunction) { $isClass = class_exists($classOrFunction) || trait_exists($classOrFunction); $isMethod = function_exists($classOrFunction); if (! $isClass && ! $isMethod) { throw new InvalidArgumentException(sprintf('No class or method named "%s" has been found.', $classOrFunction)); } if ($isClass) { $this->coversClass($classOrFunction); } else { $this->coversFunction($classOrFunction); } } return $this; } /** * Sets the covered classes. */ public function coversClass(string ...$classes): self { foreach ($classes as $class) { $this->testCaseMethod->covers[] = new CoversClass($class); } return $this; } /** * Sets the covered functions. */ public function coversFunction(string ...$functions): self { foreach ($functions as $function) { $this->testCaseMethod->covers[] = new CoversFunction($function); } return $this; } /** * Sets that the current test covers nothing. */ public function coversNothing(): self { $this->testCaseMethod->covers = [new CoversNothing()]; return $this; } /** * Informs the test runner that no expectations happen in this test, * and its purpose is simply to check whether the given code can * be executed without throwing exceptions. */ public function throwsNoExceptions(): self { $this->testCaseMethod->proxies->add(Backtrace::file(), Backtrace::line(), 'expectNotToPerformAssertions', []); return $this; } /** * Saves the property accessors to be used on the target. */ public function __get(string $name): self { return $this->addChain(Backtrace::file(), Backtrace::line(), $name); } /** * Saves the calls to be used on the target. * * @param array $arguments */ public function __call(string $name, array $arguments): self { return $this->addChain(Backtrace::file(), Backtrace::line(), $name, $arguments); } /** * Add a chain to the test case factory. Omitting the arguments will treat it as a property accessor. * * @param array|null $arguments */ private function addChain(string $file, int $line, string $name, ?array $arguments = null): self { $exporter = Exporter::default(); $this->testCaseMethod ->chains ->add($file, $line, $name, $arguments); if ($this->descriptionLess) { Exporter::default(); if ($this->testCaseMethod->description !== null) { $this->testCaseMethod->description .= ' → '; } $this->testCaseMethod->description .= $arguments === null ? $name : sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments)); } return $this; } /** * Creates the Call. */ public function __destruct() { if (! is_null($this->describing)) { $this->testCaseMethod->describing = $this->describing; $this->testCaseMethod->description = Str::describe($this->describing, $this->testCaseMethod->description); // @phpstan-ignore-line } $this->testSuite->tests->set($this->testCaseMethod); } }