Merge branch 'master' into performs_no_expectations

# Conflicts:
#	src/PendingCalls/TestCall.php
This commit is contained in:
Luke Downing
2022-03-22 13:20:31 +00:00
33 changed files with 456 additions and 47 deletions

View File

@ -20,10 +20,10 @@ jobs:
coverage: none coverage: none
- name: Install Dependencies - name: Install Dependencies
run: composer update --no-interaction --no-progress run: composer update --no-interaction --no-progress --ansi
- name: Run PHP-CS-Fixer - name: Run PHP-CS-Fixer
run: PHP_CS_FIXER_IGNORE_ENV=true vendor/bin/php-cs-fixer fix -v --allow-risky=yes --dry-run run: PHP_CS_FIXER_IGNORE_ENV=true vendor/bin/php-cs-fixer fix -v --allow-risky=yes --dry-run --ansi
phpstan: phpstan:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -45,7 +45,7 @@ jobs:
coverage: none coverage: none
- name: Install Dependencies - name: Install Dependencies
run: composer update --prefer-stable --no-interaction --no-progress run: composer update --prefer-stable --no-interaction --no-progress --ansi
- name: Run PHPStan - name: Run PHPStan
run: vendor/bin/phpstan analyse --no-progress --debug run: vendor/bin/phpstan analyse --no-progress --debug --ansi

View File

@ -36,7 +36,7 @@ jobs:
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install PHP dependencies - name: Install PHP dependencies
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress --ansi
- name: Unit Tests - name: Unit Tests
run: php bin/pest --colors=always --exclude-group=integration run: php bin/pest --colors=always --exclude-group=integration

View File

@ -1,3 +1,7 @@
<a href="https://supportukrainenow.org/"><img src="https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg" width="100%"></a>
------
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/pestphp/art/master/readme.png" width="600" alt="PEST"> <img src="https://raw.githubusercontent.com/pestphp/art/master/readme.png" width="600" alt="PEST">
<p align="center"> <p align="center">
@ -21,6 +25,7 @@ We would like to extend our thanks to the following sponsors for funding Pest de
### Platinum Sponsors ### Platinum Sponsors
- **[Advent](https://advent.dev)**
- **[Spatie](https://spatie.be)** - **[Spatie](https://spatie.be)**
- **[Worksome](https://www.worksome.com/)** - **[Worksome](https://www.worksome.com/)**

View File

@ -48,6 +48,7 @@ final class BootFiles
if (is_dir($filename)) { if (is_dir($filename)) {
$directory = new RecursiveDirectoryIterator($filename); $directory = new RecursiveDirectoryIterator($filename);
$iterator = new RecursiveIteratorIterator($directory); $iterator = new RecursiveIteratorIterator($directory);
/** @var \DirectoryIterator $file */
foreach ($iterator as $file) { foreach ($iterator as $file) {
$this->load($file->__toString()); $this->load($file->__toString());
} }

View File

@ -43,7 +43,7 @@ trait Pipeable
$this->pipe($name, function ($next, ...$arguments) use ($handler, $filter) { $this->pipe($name, function ($next, ...$arguments) use ($handler, $filter) {
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
if ($filter($this->value, ...$arguments)) { if ($filter($this->value, ...$arguments)) {
//@phpstan-ignore-next-line // @phpstan-ignore-next-line
$handler->bindTo($this, get_class($this))(...$arguments); $handler->bindTo($this, get_class($this))(...$arguments);
return; return;

View File

@ -76,6 +76,22 @@ final class Expectation
return $this->toBeJson()->and($value); return $this->toBeJson()->and($value);
} }
/**
* Dump the expectation value.
*
* @return self<TValue>
*/
public function dump(mixed ...$arguments): self
{
if (function_exists('dump')) {
dump($this->value, ...$arguments);
} else {
var_dump($this->value);
}
return $this;
}
/** /**
* Dump the expectation value and end the script. * Dump the expectation value and end the script.
* *
@ -151,7 +167,6 @@ final class Expectation
throw new BadMethodCallException('Expectation value is not iterable.'); throw new BadMethodCallException('Expectation value is not iterable.');
} }
//@phpstan-ignore-next-line
$value = is_array($this->value) ? $this->value : iterator_to_array($this->value); $value = is_array($this->value) ? $this->value : iterator_to_array($this->value);
$keys = array_keys($value); $keys = array_keys($value);
$values = array_values($value); $values = array_values($value);
@ -292,7 +307,7 @@ final class Expectation
private function getExpectationClosure(string $name): Closure private function getExpectationClosure(string $name): Closure
{ {
if (method_exists(Mixins\Expectation::class, $name)) { if (method_exists(Mixins\Expectation::class, $name)) {
//@phpstan-ignore-next-line // @phpstan-ignore-next-line
return Closure::fromCallable([new Mixins\Expectation($this->value), $name]); return Closure::fromCallable([new Mixins\Expectation($this->value), $name]);
} }

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Annotations;
use Pest\Factories\Covers\CoversNothing as CoversNothingFactory;
use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
final class CoversNothing
{
/**
* Adds annotations regarding the "depends" feature.
*
* @param array<int, string> $annotations
*
* @return array<int, string>
*/
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
{
if (($method->covers[0] ?? null) instanceof CoversNothingFactory) {
$annotations[] = '@coversNothing';
}
return $annotations;
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Attributes;
use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
abstract class Attribute
{
/**
* Determine if the attribute should be placed above the class instead of above the method.
*
* @var bool
*/
public const ABOVE_CLASS = false;
/**
* @param array<int, string> $attributes
*
* @return array<int, string>
*/
public function __invoke(TestCaseMethodFactory $method, array $attributes): array // @phpstan-ignore-line
{
return $attributes;
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Attributes;
use Pest\Factories\Covers\CoversClass;
use Pest\Factories\Covers\CoversFunction;
use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
final class Covers extends Attribute
{
/**
* Determine if the attribute should be placed above the classe instead of above the method.
*
* @var bool
*/
public const ABOVE_CLASS = true;
/**
* Adds attributes regarding the "covers" feature.
*
* @param array<int, string> $attributes
*
* @return array<int, string>
*/
public function __invoke(TestCaseMethodFactory $method, array $attributes): array
{
foreach ($method->covers as $covering) {
if ($covering instanceof CoversClass) {
// Prepend a backslash for FQN classes
if (str_contains($covering->class, '\\')) {
$covering->class = '\\' . $covering->class;
}
$attributes[] = "#[\PHPUnit\Framework\Attributes\CoversClass({$covering->class}::class)]";
} elseif ($covering instanceof CoversFunction) {
$attributes[] = "#[\PHPUnit\Framework\Attributes\CoversFunction('{$covering->function}')]";
}
}
return $attributes;
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Covers;
/**
* @internal
*/
final class CoversClass
{
public function __construct(public string $class)
{
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Covers;
/**
* @internal
*/
final class CoversFunction
{
public function __construct(public string $function)
{
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Covers;
/**
* @internal
*/
final class CoversNothing
{
}

View File

@ -33,6 +33,16 @@ final class TestCaseFactory
private static array $annotations = [ private static array $annotations = [
Annotations\Depends::class, Annotations\Depends::class,
Annotations\Groups::class, Annotations\Groups::class,
Annotations\CoversNothing::class,
];
/**
* The list of attributes.
*
* @var array<int, class-string<\Pest\Factories\Attributes\Attribute>>
*/
private static array $attributes = [
Attributes\Covers::class,
]; ];
/** /**
@ -141,11 +151,33 @@ final class TestCaseFactory
$classFQN .= $className; $classFQN .= $className;
} }
$classAvailableAttributes = array_filter(self::$attributes, fn (string $attribute) => $attribute::ABOVE_CLASS);
$methodAvailableAttributes = array_filter(self::$attributes, fn (string $attribute) => !$attribute::ABOVE_CLASS);
$classAttributes = [];
foreach ($classAvailableAttributes as $attribute) {
$classAttributes = array_reduce(
$methods,
fn (array $carry, TestCaseMethodFactory $methodFactory) => (new $attribute())->__invoke($methodFactory, $carry),
$classAttributes
);
}
$methodsCode = implode('', array_map( $methodsCode = implode('', array_map(
fn (TestCaseMethodFactory $methodFactory) => $methodFactory->buildForEvaluation($classFQN, self::$annotations), fn (TestCaseMethodFactory $methodFactory) => $methodFactory->buildForEvaluation(
$classFQN,
self::$annotations,
$methodAvailableAttributes
),
$methods $methods
)); ));
$classAttributesCode = implode('', array_map(
static fn (string $attribute) => sprintf("\n %s", $attribute),
array_unique($classAttributes),
));
try { try {
eval(" eval("
namespace $namespace; namespace $namespace;
@ -153,6 +185,7 @@ final class TestCaseFactory
use Pest\Repositories\DatasetsRepository as __PestDatasets; use Pest\Repositories\DatasetsRepository as __PestDatasets;
use Pest\TestSuite as __PestTestSuite; use Pest\TestSuite as __PestTestSuite;
$classAttributesCode
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN { final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
$traitsCode $traitsCode

View File

@ -47,6 +47,13 @@ final class TestCaseMethodFactory
*/ */
public array $groups = []; public array $groups = [];
/**
* The covered classes and functions, if any.
*
* @var array<int, \Pest\Factories\Covers\CoversClass|\Pest\Factories\Covers\CoversFunction|\Pest\Factories\Covers\CoversNothing>
*/
public array $covers = [];
/** /**
* Creates a new Factory instance. * Creates a new Factory instance.
*/ */
@ -106,9 +113,10 @@ final class TestCaseMethodFactory
/** /**
* Creates a PHPUnit method as a string ready for evaluation. * Creates a PHPUnit method as a string ready for evaluation.
* *
* @param array<int, class-string> $annotationsToUse * @param array<int, class-string> $annotationsToUse
* @param array<int, class-string<\Pest\Factories\Attributes\Attribute>> $attributesToUse
*/ */
public function buildForEvaluation(string $classFQN, array $annotationsToUse): string public function buildForEvaluation(string $classFQN, array $annotationsToUse, array $attributesToUse): string
{ {
if ($this->description === null) { if ($this->description === null) {
throw ShouldNotHappen::fromMessage('The test description may not be empty.'); throw ShouldNotHappen::fromMessage('The test description may not be empty.');
@ -122,12 +130,17 @@ final class TestCaseMethodFactory
$datasetsCode = ''; $datasetsCode = '';
$annotations = ['@test']; $annotations = ['@test'];
$attributes = [];
foreach ($annotationsToUse as $annotation) { foreach ($annotationsToUse as $annotation) {
/** @phpstan-ignore-next-line */ /** @phpstan-ignore-next-line */
$annotations = (new $annotation())->__invoke($this, $annotations); $annotations = (new $annotation())->__invoke($this, $annotations);
} }
foreach ($attributesToUse as $attribute) {
$attributes = (new $attribute())->__invoke($this, $attributes);
}
if (count($this->datasets) > 0) { if (count($this->datasets) > 0) {
$dataProviderName = $methodName . '_dataset'; $dataProviderName = $methodName . '_dataset';
$annotations[] = "@dataProvider $dataProviderName"; $annotations[] = "@dataProvider $dataProviderName";
@ -138,10 +151,15 @@ final class TestCaseMethodFactory
static fn ($annotation) => sprintf("\n * %s", $annotation), $annotations, static fn ($annotation) => sprintf("\n * %s", $annotation), $annotations,
)); ));
$attributes = implode('', array_map(
static fn ($attribute) => sprintf("\n %s", $attribute), $attributes,
));
return <<<EOF return <<<EOF
/**$annotations /**$annotations
*/ */
$attributes
public function $methodName() public function $methodName()
{ {
return \$this->__runTest( return \$this->__runTest(

View File

@ -37,7 +37,7 @@ final class Kernel
public static function boot(): self public static function boot(): self
{ {
foreach (self::$bootstrappers as $bootstrapper) { foreach (self::$bootstrappers as $bootstrapper) {
//@phpstan-ignore-next-line // @phpstan-ignore-next-line
(new $bootstrapper())->__invoke(); (new $bootstrapper())->__invoke();
} }

View File

@ -6,6 +6,7 @@ namespace Pest\Mixins;
use BadMethodCallException; use BadMethodCallException;
use Closure; use Closure;
use Error;
use InvalidArgumentException; use InvalidArgumentException;
use Pest\Exceptions\InvalidExpectationValue; use Pest\Exceptions\InvalidExpectationValue;
use Pest\Support\Arr; use Pest\Support\Arr;
@ -282,7 +283,7 @@ final class Expectation
{ {
$this->toBeObject(); $this->toBeObject();
//@phpstan-ignore-next-line // @phpstan-ignore-next-line
Assert::assertTrue(property_exists($this->value, $name)); Assert::assertTrue(property_exists($this->value, $name));
if (func_num_args() > 1) { if (func_num_args() > 1) {
@ -533,7 +534,7 @@ final class Expectation
{ {
Assert::assertIsString($this->value); Assert::assertIsString($this->value);
//@phpstan-ignore-next-line // @phpstan-ignore-next-line
Assert::assertJson($this->value); Assert::assertJson($this->value);
return $this; return $this;
@ -579,7 +580,7 @@ final class Expectation
try { try {
Assert::assertTrue(Arr::has($array, $key)); Assert::assertTrue(Arr::has($array, $key));
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
} catch (ExpectationFailedException $exception) { } catch (ExpectationFailedException $exception) {
throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure()); throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure());
} }
@ -822,8 +823,12 @@ final class Expectation
try { try {
($this->value)(); ($this->value)();
} catch (Throwable $e) { // @phpstan-ignore-line } catch (Throwable $e) {
if (!class_exists($exception)) { if (!class_exists($exception)) {
if ($e instanceof Error && $e->getMessage() === "Class \"$exception\" not found") {
throw $e;
}
Assert::assertStringContainsString($exception, $e->getMessage()); Assert::assertStringContainsString($exception, $e->getMessage());
return $this; return $this;

View File

@ -5,6 +5,10 @@ declare(strict_types=1);
namespace Pest\PendingCalls; namespace Pest\PendingCalls;
use Closure; use Closure;
use InvalidArgumentException;
use Pest\Factories\Covers\CoversClass;
use Pest\Factories\Covers\CoversFunction;
use Pest\Factories\Covers\CoversNothing;
use Pest\Factories\TestCaseMethodFactory; use Pest\Factories\TestCaseMethodFactory;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
use Pest\Support\HigherOrderCallables; use Pest\Support\HigherOrderCallables;
@ -45,9 +49,11 @@ final class TestCall
/** /**
* Asserts that the test throws the given `$exceptionClass` when called. * Asserts that the test throws the given `$exceptionClass` when called.
*/ */
public function throws(string $exception, string $exceptionMessage = null): TestCall public function throws(string|int $exception, string $exceptionMessage = null, int $exceptionCode = null): TestCall
{ {
if (class_exists($exception)) { if (is_int($exception)) {
$exceptionCode = $exception;
} elseif (class_exists($exception)) {
$this->testCaseMethod $this->testCaseMethod
->proxies ->proxies
->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exception]); ->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exception]);
@ -61,6 +67,12 @@ final class TestCall
->add(Backtrace::file(), Backtrace::line(), 'expectExceptionMessage', [$exceptionMessage]); ->add(Backtrace::file(), Backtrace::line(), 'expectExceptionMessage', [$exceptionMessage]);
} }
if (is_int($exceptionCode)) {
$this->testCaseMethod
->proxies
->add(Backtrace::file(), Backtrace::line(), 'expectExceptionCode', [$exceptionCode]);
}
return $this; return $this;
} }
@ -69,7 +81,7 @@ final class TestCall
* *
* @param (callable(): bool)|bool $condition * @param (callable(): bool)|bool $condition
*/ */
public function throwsIf(callable|bool $condition, string $exception, string $exceptionMessage = null): TestCall public function throwsIf(callable|bool $condition, string|int $exception, string $exceptionMessage = null, int $exceptionCode = null): TestCall
{ {
$condition = is_callable($condition) $condition = is_callable($condition)
? $condition ? $condition
@ -78,7 +90,7 @@ final class TestCall
}; };
if ($condition()) { if ($condition()) {
return $this->throws($exception, $exceptionMessage); return $this->throws($exception, $exceptionMessage, $exceptionCode);
} }
return $this; return $this;
@ -160,6 +172,63 @@ final class TestCall
return $this; return $this;
} }
/**
* Sets the covered classes or methods.
*/
public function covers(string ...$classesOrFunctions): TestCall
{
foreach ($classesOrFunctions as $classOrFunction) {
$isClass = class_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): TestCall
{
foreach ($classes as $class) {
$this->testCaseMethod->covers[] = new CoversClass($class);
}
return $this;
}
/**
* Sets the covered functions.
*/
public function coversFunction(string ...$functions): TestCall
{
foreach ($functions as $function) {
$this->testCaseMethod->covers[] = new CoversFunction($function);
}
return $this;
}
/**
* Sets that the current test covers nothing.
*/
public function coversNothing(): TestCall
{
$this->testCaseMethod->covers = [new CoversNothing()];
return $this;
}
/** /**
* Informs the test runner that no expectations happen in this test, * Informs the test runner that no expectations happen in this test,
* and its purpose is simply to check whether the given code can * and its purpose is simply to check whether the given code can

View File

@ -156,11 +156,11 @@ final class DatasetsRepository
$datasets[$index] = iterator_to_array($datasets[$index]); $datasets[$index] = iterator_to_array($datasets[$index]);
} }
//@phpstan-ignore-next-line // @phpstan-ignore-next-line
foreach ($datasets[$index] as $key => $values) { foreach ($datasets[$index] as $key => $values) {
$values = is_array($values) ? $values : [$values]; $values = is_array($values) ? $values : [$values];
$processedDataset[] = [ $processedDataset[] = [
'label' => self::getDatasetDescription($key, $values), //@phpstan-ignore-line 'label' => self::getDatasetDescription($key, $values), // @phpstan-ignore-line
'values' => $values, 'values' => $values,
]; ];
} }
@ -189,7 +189,7 @@ final class DatasetsRepository
$result = $tmp; $result = $tmp;
} }
//@phpstan-ignore-next-line // @phpstan-ignore-next-line
return $result; return $result;
} }

View File

@ -85,7 +85,7 @@ final class Container
} }
} }
//@phpstan-ignore-next-line // @phpstan-ignore-next-line
return $this->get($candidate); return $this->get($candidate);
}, },
$constructor->getParameters() $constructor->getParameters()

View File

@ -40,7 +40,7 @@ final class HigherOrderMessageCollection
public function chain(object $target): void public function chain(object $target): void
{ {
foreach ($this->messages as $message) { foreach ($this->messages as $message) {
//@phpstan-ignore-next-line // @phpstan-ignore-next-line
$target = $message->call($target) ?? $target; $target = $message->call($target) ?? $target;
} }
} }

View File

@ -8,7 +8,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
it('has plugin')->assertTrue(class_exists(CoveragePlugin::class)); it('has plugin')->assertTrue(class_exists(CoveragePlugin::class));
it('adds coverage if --coverage exist', function () { it('adds coverage if --coverage exist', function () {
$plugin = new CoveragePlugin(new ConsoleOutput()); $plugin = new CoveragePlugin(new ConsoleOutput());
$testSuite = TestSuite::getInstance(); $testSuite = TestSuite::getInstance();
expect($plugin->coverage)->toBeFalse(); expect($plugin->coverage)->toBeFalse();

78
tests/Features/Covers.php Normal file
View File

@ -0,0 +1,78 @@
<?php
use Pest\Factories\Attributes\Covers;
use Pest\PendingCalls\TestCall;
use Pest\TestSuite;
$runCounter = 0;
class TestCoversClass1
{
}
class TestCoversClass2
{
}
class TestCoversClass3
{
}
function testCoversFunction()
{
}
it('uses the correct PHPUnit attribute for class', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[0]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass');
expect($attributes[0]->getArguments()[0])->toBe('P\Tests\Features\TestCoversClass1');
})->coversClass(TestCoversClass1::class);
it('uses the correct PHPUnit attribute for function', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[1]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction');
expect($attributes[1]->getArguments()[0])->toBe('foo');
})->coversFunction('foo');
it('removes duplicated attributes', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[2]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass');
expect($attributes[2]->getArguments()[0])->toBe('P\Tests\Features\TestCoversClass2');
expect($attributes[3]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass');
expect($attributes[3]->getArguments()[0])->toBe('Pest\Factories\Attributes\Covers');
expect($attributes[4]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction');
expect($attributes[4]->getArguments()[0])->toBe('bar');
expect($attributes[5]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction');
expect($attributes[5]->getArguments()[0])->toBe('baz');
})
->coversClass(TestCoversClass2::class, TestCoversClass1::class, Covers::class)
->coversFunction('bar', 'foo', 'baz');
it('guesses if the given argument is a class or function', function () {
$attributes = (new ReflectionClass($this))->getAttributes();
expect($attributes[6]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass');
expect($attributes[6]->getArguments()[0])->toBe('P\Tests\Features\TestCoversClass3');
expect($attributes[7]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction');
expect($attributes[7]->getArguments()[0])->toBe('testCoversFunction');
})->covers(TestCoversClass3::class, 'testCoversFunction');
it('appends CoversNothing to method attributes', function () {
$phpDoc = (new ReflectionClass($this))->getMethod($this->getName());
expect(str_contains($phpDoc->getDocComment(), '* @coversNothing'))->toBeTrue();
})->coversNothing();
it('does not append CoversNothing to other methods', function () {
$phpDoc = (new ReflectionClass($this))->getMethod($this->getName());
expect(str_contains($phpDoc->getDocComment(), '* @coversNothing'))->toBeFalse();
});
it('throws exception if no class nor method has been found', function () {
$testCall = new TestCall(TestSuite::getInstance(), 'filename', 'description', fn () => 'closure');
$testCall->covers('fakeName');
})->throws(InvalidArgumentException::class, 'No class or method named "fakeName" has been found.');

View File

@ -14,10 +14,18 @@ it('catch exceptions and messages', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throws(Exception::class, 'Something bad happened'); })->throws(Exception::class, 'Something bad happened');
it('catch exceptions, messages and code', function () {
throw new Exception('Something bad happened', 1);
})->throws(Exception::class, 'Something bad happened', 1);
it('can just define the message', function () { it('can just define the message', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throws('Something bad happened'); })->throws('Something bad happened');
it('can just define the code', function () {
throw new Exception('Something bad happened', 1);
})->throws(1);
it('not catch exceptions if given condition is false', function () { it('not catch exceptions if given condition is false', function () {
$this->assertTrue(true); $this->assertTrue(true);
})->throwsIf(false, Exception::class); })->throwsIf(false, Exception::class);
@ -30,10 +38,22 @@ it('catch exceptions and messages if given condition is true', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throwsIf(true, Exception::class, 'Something bad happened'); })->throwsIf(true, Exception::class, 'Something bad happened');
it('catch exceptions, messages and code if given condition is true', function () {
throw new Exception('Something bad happened', 1);
})->throwsIf(true, Exception::class, 'Something bad happened', 1);
it('can just define the message if given condition is true', function () { it('can just define the message if given condition is true', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throwsIf(true, 'Something bad happened'); })->throwsIf(true, 'Something bad happened');
it('can just define the code if given condition is true', function () {
throw new Exception('Something bad happened', 1);
})->throwsIf(true, 1);
it('can just define the message if given condition is 1', function () { it('can just define the message if given condition is 1', function () {
throw new Exception('Something bad happened'); throw new Exception('Something bad happened');
})->throwsIf(1, 'Something bad happened'); })->throwsIf(1, 'Something bad happened');
it('can just define the code if given condition is 1', function () {
throw new Exception('Something bad happened', 1);
})->throwsIf(1, 1);

View File

@ -12,7 +12,7 @@ class Number
public function __construct( public function __construct(
public int $value public int $value
) { ) {
//.. // ..
} }
} }
@ -21,7 +21,7 @@ class Char
public function __construct( public function __construct(
public string $value public string $value
) { ) {
//.. // ..
} }
} }
@ -30,7 +30,7 @@ class Symbol
public function __construct( public function __construct(
public string $value public string $value
) { ) {
//.. // ..
} }
} }
@ -69,11 +69,11 @@ expect()->pipe('toBe', function ($next, $expected) use ($state) {
assertInstanceOf(Char::class, $expected); assertInstanceOf(Char::class, $expected);
assertEquals($this->value->value, $expected->value); assertEquals($this->value->value, $expected->value);
//returning nothing stops pipeline execution // returning nothing stops pipeline execution
return; return;
} }
//calling $next(); let the pipeline to keep running // calling $next(); let the pipeline to keep running
$next(); $next();
}); });

View File

@ -5,7 +5,7 @@ use PHPUnit\Framework\ExpectationFailedException;
expect(true)->toBeTrue()->and(false)->toBeFalse(); expect(true)->toBeTrue()->and(false)->toBeFalse();
test('strict comparisons', function () { test('strict comparisons', function () {
$nuno = new stdClass(); $nuno = new stdClass();
$dries = new stdClass(); $dries = new stdClass();
expect($nuno)->toBe($nuno)->not->toBe($dries); expect($nuno)->toBe($nuno)->not->toBe($dries);

View File

@ -3,24 +3,24 @@
use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\ExpectationFailedException;
test('pass', function () { test('pass', function () {
$object = new stdClass(); $object = new stdClass();
$object->name = 'Jhon'; $object->name = 'Jhon';
$object->age = 21; $object->age = 21;
expect($object)->toHaveProperties(['name', 'age']); expect($object)->toHaveProperties(['name', 'age']);
}); });
test('failures', function () { test('failures', function () {
$object = new stdClass(); $object = new stdClass();
$object->name = 'Jhon'; $object->name = 'Jhon';
expect($object)->toHaveProperties(['name', 'age']); expect($object)->toHaveProperties(['name', 'age']);
})->throws(ExpectationFailedException::class); })->throws(ExpectationFailedException::class);
test('not failures', function () { test('not failures', function () {
$object = new stdClass(); $object = new stdClass();
$object->name = 'Jhon'; $object->name = 'Jhon';
$object->age = 21; $object->age = 21;
expect($object)->not->toHaveProperties(['name', 'age']); expect($object)->not->toHaveProperties(['name', 'age']);
})->throws(ExpectationFailedException::class); })->throws(ExpectationFailedException::class);

View File

@ -58,3 +58,19 @@ test('closure missing parameter', function () {
test('closure missing type-hint', function () { test('closure missing type-hint', function () {
expect(function () {})->toThrow(function ($e) {}); expect(function () {})->toThrow(function ($e) {});
})->throws(InvalidArgumentException::class, 'The given closure\'s parameter must be type-hinted as the class string.'); })->throws(InvalidArgumentException::class, 'The given closure\'s parameter must be type-hinted as the class string.');
it('can handle a non-defined exception', function () {
expect(function () {
throw new NonExistingException();
})->toThrow(NonExistingException::class);
})->throws(Error::class, 'Class "NonExistingException" not found');
it('can handle a class not found Error', function () {
expect(function () {
throw new NonExistingException();
})->toThrow('Class "NonExistingException" not found');
expect(function () {
throw new NonExistingException();
})->toThrow(Error::class, 'Class "NonExistingException" not found');
});

View File

@ -3,9 +3,9 @@
use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\ExpectationFailedException;
beforeEach(function () { beforeEach(function () {
$this->unlessObject = new stdClass(); $this->unlessObject = new stdClass();
$this->unlessObject->trueValue = true; $this->unlessObject->trueValue = true;
$this->unlessObject->foo = 'foo'; $this->unlessObject->foo = 'foo';
}); });
it('pass', function () { it('pass', function () {

View File

@ -3,9 +3,9 @@
use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\ExpectationFailedException;
beforeEach(function () { beforeEach(function () {
$this->whenObject = new stdClass(); $this->whenObject = new stdClass();
$this->whenObject->trueValue = true; $this->whenObject->trueValue = true;
$this->whenObject->foo = 'foo'; $this->whenObject->foo = 'foo';
}); });
it('pass', function () { it('pass', function () {

View File

@ -10,7 +10,7 @@ it('gets file name from closure', function () {
}); });
it('gets property values', function () { it('gets property values', function () {
$class = new class() { $class = new class() {
private $foo = 'bar'; private $foo = 'bar';
}; };

View File

@ -8,7 +8,7 @@ use Pest\TestSuite;
it('does not allow to add the same test description twice', function () { it('does not allow to add the same test description twice', function () {
$testSuite = new TestSuite(getcwd(), 'tests'); $testSuite = new TestSuite(getcwd(), 'tests');
$method = new TestCaseMethodFactory('foo', 'bar', null); $method = new TestCaseMethodFactory('foo', 'bar', null);
$testSuite->tests->set($method); $testSuite->tests->set($method);
$testSuite->tests->set($method); $testSuite->tests->set($method);
@ -43,7 +43,7 @@ it('can return an array of all test suite filenames', function () {
it('can filter the test suite filenames to those with the only method', function () { it('can filter the test suite filenames to those with the only method', function () {
$testSuite = new TestSuite(getcwd(), 'tests'); $testSuite = new TestSuite(getcwd(), 'tests');
$testWithOnly = new TestCaseMethodFactory('a', 'b', null); $testWithOnly = new TestCaseMethodFactory('a', 'b', null);
$testWithOnly->only = true; $testWithOnly->only = true;
$testSuite->tests->set($testWithOnly); $testSuite->tests->set($testWithOnly);
@ -61,7 +61,7 @@ it('does not filter the test suite filenames to those with the only method when
$test = function () {}; $test = function () {};
$testWithOnly = new TestCaseMethodFactory('a', 'b', null); $testWithOnly = new TestCaseMethodFactory('a', 'b', null);
$testWithOnly->only = true; $testWithOnly->only = true;
$testSuite->tests->set($testWithOnly); $testSuite->tests->set($testWithOnly);

View File

@ -8,7 +8,7 @@ test('visual snapshot of help command output', function () {
if (getenv('REBUILD_SNAPSHOTS')) { if (getenv('REBUILD_SNAPSHOTS')) {
$outputBuffer = new BufferedOutput(); $outputBuffer = new BufferedOutput();
$plugin = new Help($outputBuffer); $plugin = new Help($outputBuffer);
$plugin(); $plugin();

View File

@ -2,7 +2,7 @@
test('visual snapshot of test suite on success', function () { test('visual snapshot of test suite on success', function () {
$testsPath = dirname(__DIR__); $testsPath = dirname(__DIR__);
$snapshot = implode(DIRECTORY_SEPARATOR, [ $snapshot = implode(DIRECTORY_SEPARATOR, [
$testsPath, $testsPath,
'.snapshots', '.snapshots',
'success.txt', 'success.txt',