Merge branch 'master' into throwsif

This commit is contained in:
Mert Aşan
2021-09-19 01:47:07 +03:00
committed by GitHub
22 changed files with 407 additions and 49 deletions

View File

@ -10,8 +10,9 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
php: ['7.3', '7.4', '8.0']
dependency-version: [prefer-lowest, prefer-stable]
parallel: ['', '--parallel']
name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }}
name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }} - ${{ matrix.parallel }}
steps:
- name: Checkout
@ -38,7 +39,7 @@ jobs:
if: "matrix.php >= 8"
- name: Unit Tests
run: php bin/pest --colors=always --exclude-group=integration
run: php bin/pest --colors=always --exclude-group=integration ${{ matrix.parallel }}
- name: Integration Tests
run: php bin/pest --colors=always --group=integration

View File

@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [v1.18.0 (2021-08-30)](https://github.com/pestphp/pest/compare/v1.17.0...v1.18.0)
### Added
- `toHaveLength` expectation ([#386](https://github.com/pestphp/pest/pull/386))
- `nunomaduro/collision:^6.0` support ([4ae482c](https://github.com/pestphp/pest/commit/4ae482c7073fb77782b8a4b5738ef1fcea0f82ab))
## [v1.17.0 (2021-08-26)](https://github.com/pestphp/pest/compare/v1.16.0...v1.17.0)
### Added
- `toThrow` expectation ([#361](https://github.com/pestphp/pest/pull/361))
## [v1.16.0 (2021-08-19)](https://github.com/pestphp/pest/compare/v1.15.0...v1.16.0)
### Added
- Support for new parallel options ([#369](https://github.com/pestphp/pest/pull/369))
## [v1.15.0 (2021-08-04)](https://github.com/pestphp/pest/compare/v1.14.0...v1.15.0)
### Added
- `toBeTruthy` and `toBeFalsy` ([#367](https://github.com/pestphp/pest/pull/367))

View File

@ -31,6 +31,10 @@ composer lint
```
## Tests
Update the snapshots:
```bash
composer update:snapshots
```
Run all tests:
```bash
composer test

View File

@ -22,9 +22,11 @@ We would like to extend our thanks to the following sponsors for funding Pest de
### Premium Sponsors
- **[Akaunting](https://akaunting.com)**
- **[Auth0](https://auth0.com)**
- **[Codecourse](https://codecourse.com/)**
- **[Meema](https://meema.io/)**
- **[Fathom Analytics](https://usefathom.com/)**
- **[Meema](https://meema.io)**
- **[Scout APM](https://scoutapm.com)**
- **[Spatie](https://spatie.be/)**
- **[Spatie](https://spatie.be)**
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.

View File

@ -3,9 +3,9 @@
use NunoMaduro\Collision\Provider;
use Pest\Actions\ValidatesEnvironment;
use Pest\Console\Command;
use Pest\Support\Container;
use Pest\TestSuite;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
@ -42,14 +42,22 @@ use Symfony\Component\Console\Output\OutputInterface;
ValidatesEnvironment::in($testSuite);
$args = $_SERVER['argv'];
// Let's remove any arguments that PHPUnit does not understand
if ($argv->hasParameterOption('--test-directory')) {
foreach ($_SERVER['argv'] as $key => $value) {
foreach ($args as $key => $value) {
if (strpos($value, '--test-directory') !== false) {
unset($_SERVER['argv'][$key]);
unset($args[$key]);
}
}
}
exit($container->get(Command::class)->run($_SERVER['argv']));
if (($runInParallel = $argv->hasParameterOption(['--parallel', '-p'])) && !class_exists(\Pest\Parallel\Command::class)) {
$output->writeln("Parallel support requires the Pest Parallel plugin. Run <fg=yellow;options=bold>`composer require --dev pestphp/pest-plugin-parallel`</> first.");
exit(Command::FAILURE);
}
$command = $runInParallel ? \Pest\Parallel\Command::class : \Pest\Console\Command::class;
exit($container->get($command)->run($args));
})();

View File

@ -18,7 +18,7 @@
],
"require": {
"php": "^7.3 || ^8.0",
"nunomaduro/collision": "^5.4.0",
"nunomaduro/collision": "^5.4.0|^6.0",
"pestphp/pest-plugin": "^1.0.0",
"phpunit/phpunit": "^9.5.5"
},
@ -43,7 +43,8 @@
"illuminate/console": "^8.47.0",
"illuminate/support": "^8.47.0",
"laravel/dusk": "^6.15.0",
"pestphp/pest-dev-tools": "dev-master"
"pestphp/pest-dev-tools": "dev-master",
"pestphp/pest-plugin-parallel": "^1.0"
},
"minimum-stability": "dev",
"prefer-stable": true,
@ -59,12 +60,14 @@
"test:lint": "php-cs-fixer fix -v --dry-run",
"test:types": "phpstan analyse --ansi --memory-limit=-1",
"test:unit": "php bin/pest --colors=always --exclude-group=integration",
"test:parallel": "php bin/pest -p --colors=always --exclude-group=integration",
"test:integration": "php bin/pest --colors=always --group=integration",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always",
"test": [
"@test:lint",
"@test:types",
"@test:unit",
"@test:parallel",
"@test:integration"
]
},

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Pest\Actions;
use Pest\Contracts\Plugins\AddsOutput;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Plugin\Loader;
/**
* @internal
*/
final class InteractsWithPlugins
{
/**
* Transform the input arguments by passing it to the relevant plugins.
*
* @param array<int, string> $argv
*
* @return array<int, string>
*/
public static function handleArguments(array $argv): array
{
$plugins = Loader::getPlugins(HandlesArguments::class);
/** @var HandlesArguments $plugin */
foreach ($plugins as $plugin) {
$argv = $plugin->handleArguments($argv);
}
return $argv;
}
/**
* Provides an opportunity for any plugins that want
* to provide additional output after test execution.
*/
public static function addOutput(int $result): int
{
$plugins = Loader::getPlugins(AddsOutput::class);
/** @var AddsOutput $plugin */
foreach ($plugins as $plugin) {
$result = $plugin->addOutput($result);
}
return $result;
}
}

View File

@ -6,11 +6,9 @@ namespace Pest\Console;
use Pest\Actions\AddsDefaults;
use Pest\Actions\AddsTests;
use Pest\Actions\InteractsWithPlugins;
use Pest\Actions\LoadStructure;
use Pest\Actions\ValidatesConfiguration;
use Pest\Contracts\Plugins\AddsOutput;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Plugin\Loader;
use Pest\Plugins\Version;
use Pest\Support\Container;
use Pest\TestSuite;
@ -57,23 +55,12 @@ final class Command extends BaseCommand
*/
protected function handleArguments(array $argv): void
{
/*
* First, let's call all plugins that want to handle arguments
*/
$plugins = Loader::getPlugins(HandlesArguments::class);
$argv = InteractsWithPlugins::handleArguments($argv);
/** @var HandlesArguments $plugin */
foreach ($plugins as $plugin) {
$argv = $plugin->handleArguments($argv);
}
/*
* Next, as usual, let's send the console arguments to PHPUnit.
*/
parent::handleArguments($argv);
/*
* Finally, let's validate the configuration. Making
* Let's validate the configuration. Making
* sure all options are yet supported by Pest.
*/
ValidatesConfiguration::in($this->arguments);
@ -128,16 +115,7 @@ final class Command extends BaseCommand
LoadStructure::in($this->testSuite->rootPath);
$result = parent::run($argv, false);
/*
* Let's call all plugins that want to add output after test execution
*/
$plugins = Loader::getPlugins(AddsOutput::class);
/** @var AddsOutput $plugin */
foreach ($plugins as $plugin) {
$result = $plugin->addOutput($result);
}
$result = InteractsWithPlugins::addOutput($result);
exit($result);
}

View File

@ -5,13 +5,19 @@ declare(strict_types=1);
namespace Pest;
use BadMethodCallException;
use Closure;
use InvalidArgumentException;
use Pest\Concerns\Extendable;
use Pest\Concerns\RetrievesValues;
use Pest\Support\Arr;
use Pest\Support\NullClosure;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\ExpectationFailedException;
use ReflectionFunction;
use ReflectionNamedType;
use SebastianBergmann\Exporter\Exporter;
use Throwable;
/**
* @internal
@ -321,6 +327,36 @@ final class Expectation
return $this;
}
/**
* Asserts that $number matches value's Length.
*/
public function toHaveLength(int $number): Expectation
{
if (is_string($this->value)) {
Assert::assertEquals($number, mb_strlen($this->value));
return $this;
}
if (is_iterable($this->value)) {
return $this->toHaveCount($number);
}
if (is_object($this->value)) {
if (method_exists($this->value, 'toArray')) {
$array = $this->value->toArray();
} else {
$array = (array) $this->value;
}
Assert::assertCount($number, $array);
return $this;
}
throw new BadMethodCallException('Expectation value length is not countable.');
}
/**
* Asserts that $count matches the number of elements of the value.
*/
@ -350,6 +386,20 @@ final class Expectation
return $this;
}
/**
* Asserts that the value contains the provided properties $names.
*
* @param iterable<array-key, string> $names
*/
public function toHaveProperties(iterable $names): Expectation
{
foreach ($names as $name) {
$this->toHaveProperty($name);
}
return $this;
}
/**
* Asserts that two variables have the same value.
*
@ -749,6 +799,56 @@ final class Expectation
return $this;
}
/**
* Asserts that executing value throws an exception.
*
* @param (Closure(Throwable): mixed)|string $exception
*/
public function toThrow($exception, string $exceptionMessage = null): Expectation
{
$callback = NullClosure::create();
if ($exception instanceof Closure) {
$callback = $exception;
$parameters = (new ReflectionFunction($exception))->getParameters();
if (1 !== count($parameters)) {
throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.');
}
if (!($type = $parameters[0]->getType()) instanceof ReflectionNamedType) {
throw new InvalidArgumentException('The given closure\'s parameter must be type-hinted as the class string.');
}
$exception = $type->getName();
}
try {
($this->value)();
} catch (Throwable $e) { // @phpstan-ignore-line
if (!class_exists($exception)) {
Assert::assertStringContainsString($exception, $e->getMessage());
return $this;
}
if ($exceptionMessage !== null) {
Assert::assertStringContainsString($exceptionMessage, $e->getMessage());
}
Assert::assertInstanceOf($exception, $e);
$callback($e);
return $this;
}
if (!class_exists($exception)) {
throw new ExpectationFailedException("Exception with message \"{$exception}\" not thrown.");
}
throw new ExpectationFailedException("Exception \"{$exception}\" not thrown.");
}
/**
* Exports the given value.
*

View File

@ -286,7 +286,7 @@ final class TeamCity extends DefaultResultPrinter
{
$this->markAsFailure($t);
$this->writeWarning($test->getName());
$this->phpunitTeamCity->addSkippedTest($test, $t, $time);
$this->phpunitTeamCity->printIgnoredTest($test->getName(), $t, $time);
}
private function markAsFailure(Throwable $t): void

View File

@ -6,7 +6,7 @@ namespace Pest;
function version(): string
{
return '1.15.0';
return '1.18.0';
}
function testDirectory(string $file = ''): string

View File

@ -22,7 +22,7 @@ use PHPUnit\Framework\TestCase;
final class TestRepository
{
/**
* @var string
* @var non-empty-string
*/
private const SEPARATOR = '>>>';
@ -44,6 +44,20 @@ final class TestRepository
return count($this->state);
}
/**
* Returns the filename of each test that should be executed in the suite.
*
* @return array<int, string>
*/
public function getFilenames(): array
{
$testsWithOnly = $this->testsUsingOnly();
return array_values(array_map(function (TestCaseFactory $factory): string {
return $factory->filename;
}, count($testsWithOnly) > 0 ? $testsWithOnly : $this->state));
}
/**
* Calls the given callable foreach test case.
*/
@ -85,9 +99,7 @@ final class TestRepository
}
}
$onlyState = array_filter($this->state, function ($testFactory): bool {
return $testFactory->only;
});
$onlyState = $this->testsUsingOnly();
$state = count($onlyState) > 0 ? $onlyState : $this->state;
@ -100,6 +112,18 @@ final class TestRepository
}
}
/**
* Return all tests that have called the only method.
*
* @return array<TestCaseFactory>
*/
private function testsUsingOnly(): array
{
return array_filter($this->state, function ($testFactory): bool {
return $testFactory->only;
});
}
/**
* Uses the given `$testCaseClass` on the given `$paths`.
*

View File

@ -422,6 +422,25 @@
PASS Tests\Features\Expect\toHaveKeys
✓ pass
✓ failures
✓ not failures
PASS Tests\Features\Expect\toHaveLength
✓ it passes with ('Fortaleza')
✓ it passes with ('Sollefteå')
✓ it passes with ('Ιεράπετρα')
✓ it passes with (stdClass Object (...))
✓ it passes with (Illuminate\Support\Collection Object (...))
✓ it passes with array
✓ it passes with *not*
✓ it properly fails with *not*
✓ it fails with (1)
✓ it fails with (1.5)
✓ it fails with (true)
✓ it fails with (null)
PASS Tests\Features\Expect\toHaveProperties
✓ pass
✓ failures
✓ not failures
PASS Tests\Features\Expect\toHaveProperty
@ -454,6 +473,19 @@
✓ failures
✓ not failures
PASS Tests\Features\Expect\toThrow
✓ passes
✓ failures 1
✓ failures 2
✓ failures 3
✓ failures 4
✓ failures 5
✓ failures 6
✓ failures 7
✓ not failures
✓ closure missing parameter
✓ closure missing type-hint
PASS Tests\Features\Helpers
✓ it can set/get properties on $this
✓ it throws error if property do not exist
@ -619,6 +651,8 @@
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
✓ it can return an array of all test suite filenames
✓ it can filter the test suite filenames to those with the only method
PASS Tests\Visual\Help
✓ visual snapshot of help command output
@ -652,5 +686,5 @@
✓ it is a test
✓ it uses correct parent class
Tests: 4 incompleted, 9 skipped, 424 passed
Tests: 4 incompleted, 9 skipped, 447 passed

View File

@ -0,0 +1,27 @@
<?php
use PHPUnit\Framework\ExpectationFailedException;
it('passes', function ($value) {
expect($value)->toHaveLength(9);
})->with([
'Fortaleza', 'Sollefteå', 'Ιεράπετρα',
(object) [1, 2, 3, 4, 5, 6, 7, 8, 9],
collect([1, 2, 3, 4, 5, 6, 7, 8, 9]),
]);
it('passes with array', function () {
expect([1, 2, 3])->toHaveLength(3);
});
it('passes with *not*', function () {
expect('')->not->toHaveLength(1);
});
it('properly fails with *not*', function () {
expect('pest')->not->toHaveLength(4);
})->throws(ExpectationFailedException::class);
it('fails', function ($value) {
expect($value)->toHaveLength(1);
})->with([1, 1.5, true, null])->throws(BadMethodCallException::class);

View File

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

View File

@ -0,0 +1,60 @@
<?php
use PHPUnit\Framework\ExpectationFailedException;
test('passes', function () {
expect(function () { throw new RuntimeException(); })->toThrow(RuntimeException::class);
expect(function () { throw new RuntimeException(); })->toThrow(Exception::class);
expect(function () { throw new RuntimeException(); })->toThrow(function (RuntimeException $e) {});
expect(function () { throw new RuntimeException('actual message'); })->toThrow(function (Exception $e) {
expect($e->getMessage())->toBe('actual message');
});
expect(function () {})->not->toThrow(Exception::class);
expect(function () { throw new RuntimeException('actual message'); })->toThrow('actual message');
expect(function () { throw new Exception(); })->not->toThrow(RuntimeException::class);
expect(function () { throw new RuntimeException('actual message'); })->toThrow(RuntimeException::class, 'actual message');
expect(function () { throw new RuntimeException('actual message'); })->toThrow(function (RuntimeException $e) {}, 'actual message');
});
test('failures 1', function () {
expect(function () {})->toThrow(RuntimeException::class);
})->throws(ExpectationFailedException::class, 'Exception "' . RuntimeException::class . '" not thrown.');
test('failures 2', function () {
expect(function () {})->toThrow(function (RuntimeException $e) {});
})->throws(ExpectationFailedException::class, 'Exception "' . RuntimeException::class . '" not thrown.');
test('failures 3', function () {
expect(function () { throw new Exception(); })->toThrow(function (RuntimeException $e) {});
})->throws(ExpectationFailedException::class, 'Failed asserting that Exception Object');
test('failures 4', function () {
expect(function () { throw new Exception('actual message'); })
->toThrow(function (Exception $e) {
expect($e->getMessage())->toBe('expected message');
});
})->throws(ExpectationFailedException::class, 'Failed asserting that two strings are identical');
test('failures 5', function () {
expect(function () { throw new Exception('actual message'); })->toThrow('expected message');
})->throws(ExpectationFailedException::class, 'Failed asserting that \'actual message\' contains "expected message".');
test('failures 6', function () {
expect(function () {})->toThrow('actual message');
})->throws(ExpectationFailedException::class, 'Exception with message "actual message" not thrown');
test('failures 7', function () {
expect(function () { throw new RuntimeException('actual message'); })->toThrow(RuntimeException::class, 'expected message');
})->throws(ExpectationFailedException::class);
test('not failures', function () {
expect(function () { throw new RuntimeException(); })->not->toThrow(RuntimeException::class);
})->throws(ExpectationFailedException::class);
test('closure missing parameter', function () {
expect(function () {})->toThrow(function () {});
})->throws(InvalidArgumentException::class, 'The given closure must have a single parameter type-hinted as the class string.');
test('closure missing type-hint', function () {
expect(function () {})->toThrow(function ($e) {});
})->throws(InvalidArgumentException::class, 'The given closure\'s parameter must be type-hinted as the class string.');

View File

@ -7,7 +7,7 @@ global $globalHook;
// HACK: we have to determine our $globalHook->calls baseline. This is because
// two other tests are executed before this one due to filename ordering.
$args = $_SERVER['argv'] ?? [];
$single = isset($args[1]) && Str::endsWith(__FILE__, $args[1]);
$single = (isset($args[1]) && Str::endsWith(__FILE__, $args[1])) || ($_SERVER['PEST_PARALLEL'] ?? false);
$offset = $single ? 0 : 2;
uses()->beforeAll(function () use ($globalHook, $offset) {

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Tests\SubFolder\SubFolder\SubFolder;
namespace Tests\CustomTestCaseInSubFolders\SubFolder\SubFolder;
use PHPUnit\Framework\TestCase;

View File

@ -1,3 +0,0 @@
<?php
uses(Tests\SubFolder\SubFolder\SubFolder\CustomTestCaseInSubFolder::class)->in('CustomTestCaseInSubFolders/SubFolder');

View File

@ -1,5 +1,9 @@
<?php
use Tests\CustomTestCaseInSubFolders\SubFolder\SubFolder\CustomTestCaseInSubFolder;
uses(CustomTestCaseInSubFolder::class)->in('PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder');
uses()->group('integration')->in('Visual');
// NOTE: global test value container to be mutated and checked across files, as needed

View File

@ -22,3 +22,30 @@ it('alerts users about tests with arguments but no input', function () {
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__),
);
it('can return an array of all test suite filenames', function () {
$testSuite = new TestSuite(getcwd(), 'tests');
$test = function () {};
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test));
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'bar', $test));
expect($testSuite->tests->getFilenames())->toEqual([
__FILE__,
__FILE__,
]);
});
it('can filter the test suite filenames to those with the only method', function () {
$testSuite = new TestSuite(getcwd(), 'tests');
$test = function () {};
$testWithOnly = new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test);
$testWithOnly->only = true;
$testSuite->tests->set($testWithOnly);
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory('Baz/Bar/Boo.php', 'bar', $test));
expect($testSuite->tests->getFilenames())->toEqual([
__FILE__,
]);
});

View File

@ -9,7 +9,7 @@ test('visual snapshot of test suite on success', function () {
]);
$output = function () use ($testsPath) {
$process = (new Symfony\Component\Process\Process(['php', 'bin/pest'], dirname($testsPath), ['EXCLUDE' => 'integration', 'REBUILD_SNAPSHOTS' => false]));
$process = (new Symfony\Component\Process\Process(['php', 'bin/pest'], dirname($testsPath), ['EXCLUDE' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0]));
$process->run();