diff --git a/CHANGELOG.md b/CHANGELOG.md index f1cb37d8..ebf3c155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ 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.1.0 (2021-05-02)](https://github.com/pestphp/pest/compare/v1.0.5...v1.1.0) +### Added +- Possibility of "hooks" being added using the "uses" function ([#282](https://github.com/pestphp/pest/pull/282)) + ## [v1.0.5 (2021-03-31)](https://github.com/pestphp/pest/compare/v1.0.4...v1.0.5) ### Added - Add `--browse` option to `pest:dusk` command ([#280](https://github.com/pestphp/pest/pull/280)) diff --git a/README.md b/README.md index 45706ddd..765fcafe 100644 --- a/README.md +++ b/README.md @@ -23,5 +23,6 @@ We would like to extend our thanks to the following sponsors for funding Pest de - **[Scout APM](https://scoutapm.com)** - **[Akaunting](https://akaunting.com)** +- **[Meema](https://meema.io/)** Pest was created by **[Nuno Maduro](https://twitter.com/enunomaduro)** under the **[Sponsorware license](https://github.com/sponsorware/docs)**. It got open-sourced and is now licensed under the **[MIT license](https://opensource.org/licenses/MIT)**. diff --git a/src/Concerns/TestCase.php b/src/Concerns/TestCase.php index 9581ae57..1272d49c 100644 --- a/src/Concerns/TestCase.php +++ b/src/Concerns/TestCase.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Pest\Concerns; use Closure; +use Pest\Support\ChainableClosure; use Pest\Support\ExceptionTrace; use Pest\TestSuite; use PHPUnit\Framework\ExecutionOrderDependency; @@ -34,6 +35,38 @@ trait TestCase */ private $__test; + /** + * Holds a global/shared beforeEach ("set up") closure if one has been + * defined. + * + * @var Closure|null + */ + private $beforeEach = null; + + /** + * Holds a global/shared afterEach ("tear down") closure if one has been + * defined. + * + * @var Closure|null + */ + private $afterEach = null; + + /** + * Holds a global/shared beforeAll ("set up before") closure if one has been + * defined. + * + * @var Closure|null + */ + private static $beforeAll = null; + + /** + * Holds a global/shared afterAll ("tear down after") closure if one has + * been defined. + * + * @var Closure|null + */ + private static $afterAll = null; + /** * Creates a new instance of the test case. */ @@ -73,6 +106,68 @@ trait TestCase $this->setDependencies($tests); } + /** + * Add a shared/"global" before all test hook that will execute **before** + * the test defined `beforeAll` hook(s). + */ + public function addBeforeAll(?Closure $hook): void + { + if (!$hook) { + return; + } + + self::$beforeAll = (self::$beforeAll instanceof Closure) + ? ChainableClosure::fromStatic(self::$beforeAll, $hook) + : $hook; + } + + /** + * Add a shared/"global" after all test hook that will execute **before** + * the test defined `afterAll` hook(s). + */ + public function addAfterAll(?Closure $hook): void + { + if (!$hook) { + return; + } + + self::$afterAll = (self::$afterAll instanceof Closure) + ? ChainableClosure::fromStatic(self::$afterAll, $hook) + : $hook; + } + + /** + * Add a shared/"global" before each test hook that will execute **before** + * the test defined `beforeEach` hook. + */ + public function addBeforeEach(?Closure $hook): void + { + $this->addHook('beforeEach', $hook); + } + + /** + * Add a shared/"global" after each test hook that will execute **before** + * the test defined `afterEach` hook. + */ + public function addAfterEach(?Closure $hook): void + { + $this->addHook('afterEach', $hook); + } + + /** + * Add a shared/global hook and compose them if more than one is passed. + */ + private function addHook(string $property, ?Closure $hook): void + { + if (!$hook) { + return; + } + + $this->{$property} = ($this->{$property} instanceof Closure) + ? ChainableClosure::from($this->{$property}, $hook) + : $hook; + } + /** * Returns the test case name. Note that, in Pest * we ignore withDataset argument as the description @@ -97,6 +192,10 @@ trait TestCase $beforeAll = TestSuite::getInstance()->beforeAll->get(self::$__filename); + if (self::$beforeAll instanceof Closure) { + $beforeAll = ChainableClosure::fromStatic(self::$beforeAll, $beforeAll); + } + call_user_func(Closure::bind($beforeAll, null, self::class)); } @@ -107,6 +206,10 @@ trait TestCase { $afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename); + if (self::$afterAll instanceof Closure) { + $afterAll = ChainableClosure::fromStatic(self::$afterAll, $afterAll); + } + call_user_func(Closure::bind($afterAll, null, self::class)); parent::tearDownAfterClass(); @@ -123,6 +226,10 @@ trait TestCase $beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename); + if ($this->beforeEach instanceof Closure) { + $beforeEach = ChainableClosure::from($this->beforeEach, $beforeEach); + } + $this->__callClosure($beforeEach, func_get_args()); } @@ -133,6 +240,10 @@ trait TestCase { $afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename); + if ($this->afterEach instanceof Closure) { + $afterEach = ChainableClosure::from($this->afterEach, $afterEach); + } + $this->__callClosure($afterEach, func_get_args()); parent::tearDown(); diff --git a/src/Console/Command.php b/src/Console/Command.php index cfd8f0d4..1d6ef21c 100644 --- a/src/Console/Command.php +++ b/src/Console/Command.php @@ -90,8 +90,6 @@ final class Command extends BaseCommand */ $this->arguments = AddsDefaults::to($this->arguments); - LoadStructure::in($this->testSuite->rootPath); - $testRunner = new TestRunner($this->arguments['loader']); $testSuite = $this->arguments['test']; @@ -127,6 +125,8 @@ final class Command extends BaseCommand */ public function run(array $argv, bool $exit = true): int { + LoadStructure::in($this->testSuite->rootPath); + $result = parent::run($argv, false); /* diff --git a/src/PendingObjects/UsesCall.php b/src/PendingObjects/UsesCall.php index 935e3f0d..2078926a 100644 --- a/src/PendingObjects/UsesCall.php +++ b/src/PendingObjects/UsesCall.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Pest\PendingObjects; +use Closure; use Pest\TestSuite; /** @@ -11,6 +12,20 @@ use Pest\TestSuite; */ final class UsesCall { + /** + * Contains a global before each hook closure to be executed. + * + * Array indices here matter. They are mapped as follows: + * + * - `0` => `beforeAll` + * - `1` => `beforeEach` + * - `2` => `afterEach` + * - `3` => `afterAll` + * + * @var array + */ + private $hooks = []; + /** * Holds the class and traits. * @@ -95,11 +110,56 @@ final class UsesCall return $this; } + /** + * Sets the global beforeAll test hook. + */ + public function beforeAll(Closure $hook): UsesCall + { + $this->hooks[0] = $hook; + + return $this; + } + + /** + * Sets the global beforeEach test hook. + */ + public function beforeEach(Closure $hook): UsesCall + { + $this->hooks[1] = $hook; + + return $this; + } + + /** + * Sets the global afterEach test hook. + */ + public function afterEach(Closure $hook): UsesCall + { + $this->hooks[2] = $hook; + + return $this; + } + + /** + * Sets the global afterAll test hook. + */ + public function afterAll(Closure $hook): UsesCall + { + $this->hooks[3] = $hook; + + return $this; + } + /** * Dispatch the creation of uses. */ public function __destruct() { - TestSuite::getInstance()->tests->use($this->classAndTraits, $this->groups, $this->targets); + TestSuite::getInstance()->tests->use( + $this->classAndTraits, + $this->groups, + $this->targets, + $this->hooks, + ); } } diff --git a/src/Pest.php b/src/Pest.php index 6e63d622..cc7bca60 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,5 +6,5 @@ namespace Pest; function version(): string { - return '1.0.5'; + return '1.1.0'; } diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index cf36319e..ab49291e 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Pest\Repositories; +use Closure; use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\TestAlreadyExist; use Pest\Exceptions\TestCaseAlreadyInUse; @@ -24,7 +25,7 @@ final class TestRepository private $state = []; /** - * @var array>> + * @var array>> */ private $uses = []; @@ -46,12 +47,13 @@ final class TestRepository }; foreach ($this->uses as $path => $uses) { - [$classOrTraits, $groups] = $uses; - $setClassName = function (TestCaseFactory $testCase, string $key) use ($path, $classOrTraits, $groups, $startsWith): void { + [$classOrTraits, $groups, $hooks] = $uses; + + $setClassName = function (TestCaseFactory $testCase, string $key) use ($path, $classOrTraits, $groups, $startsWith, $hooks): void { [$filename] = explode('@', $key); if ((!is_dir($path) && $filename === $path) || (is_dir($path) && $startsWith($filename, $path))) { - foreach ($classOrTraits as $class) { + foreach ($classOrTraits as $class) { /** @var string $class */ if (class_exists($class)) { if ($testCase->class !== TestCase::class) { throw new TestCaseAlreadyInUse($testCase->class, $class, $filename); @@ -62,10 +64,12 @@ final class TestRepository } } - $testCase - ->factoryProxies - // Consider set the real line here. - ->add($filename, 0, 'addGroups', [$groups]); + // IDEA: Consider set the real lines on these. + $testCase->factoryProxies->add($filename, 0, 'addGroups', [$groups]); + $testCase->factoryProxies->add($filename, 0, 'addBeforeAll', [$hooks[0] ?? null]); + $testCase->factoryProxies->add($filename, 0, 'addBeforeEach', [$hooks[1] ?? null]); + $testCase->factoryProxies->add($filename, 0, 'addAfterEach', [$hooks[2] ?? null]); + $testCase->factoryProxies->add($filename, 0, 'addAfterAll', [$hooks[3] ?? null]); } }; @@ -81,7 +85,7 @@ final class TestRepository $state = count($onlyState) > 0 ? $onlyState : $this->state; foreach ($state as $testFactory) { - /* @var TestCaseFactory $testFactory */ + /** @var TestCaseFactory $testFactory */ $tests = $testFactory->build($testSuite); foreach ($tests as $test) { $each($test); @@ -92,11 +96,12 @@ final class TestRepository /** * Uses the given `$testCaseClass` on the given `$paths`. * - * @param array $classOrTraits - * @param array $groups - * @param array $paths + * @param array $classOrTraits + * @param array $groups + * @param array $paths + * @param array $hooks */ - public function use(array $classOrTraits, array $groups, array $paths): void + public function use(array $classOrTraits, array $groups, array $paths, array $hooks): void { foreach ($classOrTraits as $classOrTrait) { if (!class_exists($classOrTrait) && !trait_exists($classOrTrait)) { @@ -109,9 +114,10 @@ final class TestRepository $this->uses[$path] = [ array_merge($this->uses[$path][0], $classOrTraits), array_merge($this->uses[$path][1], $groups), + $this->uses[$path][2] + $hooks, // NOTE: array_merge will destroy numeric indices ]; } else { - $this->uses[$path] = [$classOrTraits, $groups]; + $this->uses[$path] = [$classOrTraits, $groups, $hooks]; } } } diff --git a/src/Support/ChainableClosure.php b/src/Support/ChainableClosure.php index fea5533b..0dc40275 100644 --- a/src/Support/ChainableClosure.php +++ b/src/Support/ChainableClosure.php @@ -12,7 +12,7 @@ use Closure; final class ChainableClosure { /** - * Calls the given `$closure` and chains the the `$next` closure. + * Calls the given `$closure` and chains the `$next` closure. */ public static function from(Closure $closure, Closure $next): Closure { @@ -23,4 +23,17 @@ final class ChainableClosure call_user_func_array(Closure::bind($next, $this, get_class($this)), func_get_args()); }; } + + /** + * Call the given static `$closure` and chains the `$next` closure. + */ + public static function fromStatic(Closure $closure, Closure $next): Closure + { + return static function () use ($closure, $next): void { + /* @phpstan-ignore-next-line */ + call_user_func_array(Closure::bind($closure, null, self::class), func_get_args()); + /* @phpstan-ignore-next-line */ + call_user_func_array(Closure::bind($next, null, self::class), func_get_args()); + }; + } } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index f4db6877..625557a8 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -103,6 +103,18 @@ PASS Tests\Fixtures\ExampleTest ✓ it example 2 + PASS Tests\Hooks\AfterAllTest + ✓ global afterAll execution order + + PASS Tests\Hooks\AfterEachTest + ✓ global afterEach execution order + + PASS Tests\Hooks\BeforeAllTest + ✓ global beforeAll execution order + + PASS Tests\Hooks\BeforeEachTest + ✓ global beforeEach execution order + PASS Tests\PHPUnit\CustomAffixes\InvalidTestName ✓ it runs file names like `@#$%^&()-_=+.php` @@ -209,5 +221,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 7 skipped, 115 passed + Tests: 7 skipped, 119 passed \ No newline at end of file diff --git a/tests/Hooks/AfterAllTest.php b/tests/Hooks/AfterAllTest.php new file mode 100644 index 00000000..a34a5847 --- /dev/null +++ b/tests/Hooks/AfterAllTest.php @@ -0,0 +1,27 @@ +afterAll(function () use ($globalHook) { + expect($globalHook) + ->toHaveProperty('afterAll') + ->and($globalHook->afterAll) + ->toBe(0); + + $globalHook->afterAll = 1; +}); + +afterAll(function () use ($globalHook) { + expect($globalHook) + ->toHaveProperty('afterAll') + ->and($globalHook->afterAll) + ->toBe(1); + + $globalHook->afterAll = 2; +}); + +test('global afterAll execution order', function () use ($globalHook) { + expect($globalHook) + ->not() + ->toHaveProperty('afterAll'); +}); diff --git a/tests/Hooks/AfterEachTest.php b/tests/Hooks/AfterEachTest.php new file mode 100644 index 00000000..41dc6692 --- /dev/null +++ b/tests/Hooks/AfterEachTest.php @@ -0,0 +1,23 @@ +afterEach(function () { + expect($this) + ->toHaveProperty('ith') + ->and($this->ith) + ->toBe(0); + + $this->ith = 1; +}); + +afterEach(function () { + expect($this) + ->toHaveProperty('ith') + ->and($this->ith) + ->toBe(1); +}); + +test('global afterEach execution order', function () { + expect($this) + ->not() + ->toHaveProperty('ith'); +}); diff --git a/tests/Hooks/BeforeAllTest.php b/tests/Hooks/BeforeAllTest.php new file mode 100644 index 00000000..11c996c5 --- /dev/null +++ b/tests/Hooks/BeforeAllTest.php @@ -0,0 +1,28 @@ +beforeAll(function () use ($globalHook) { + expect($globalHook) + ->toHaveProperty('beforeAll') + ->and($globalHook->beforeAll) + ->toBe(0); + + $globalHook->beforeAll = 1; +}); + +beforeAll(function () use ($globalHook) { + expect($globalHook) + ->toHaveProperty('beforeAll') + ->and($globalHook->beforeAll) + ->toBe(1); + + $globalHook->beforeAll = 2; +}); + +test('global beforeAll execution order', function () use ($globalHook) { + expect($globalHook) + ->toHaveProperty('beforeAll') + ->and($globalHook->beforeAll) + ->toBe(2); +}); diff --git a/tests/Hooks/BeforeEachTest.php b/tests/Hooks/BeforeEachTest.php new file mode 100644 index 00000000..a9317cef --- /dev/null +++ b/tests/Hooks/BeforeEachTest.php @@ -0,0 +1,26 @@ +beforeEach(function () { + expect($this) + ->toHaveProperty('baz') + ->and($this->baz) + ->toBe(0); + + $this->baz = 1; +}); + +beforeEach(function () { + expect($this) + ->toHaveProperty('baz') + ->and($this->baz) + ->toBe(1); + + $this->baz = 2; +}); + +test('global beforeEach execution order', function () { + expect($this) + ->toHaveProperty('baz') + ->and($this->baz) + ->toBe(2); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 2090100f..a8cd868d 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,3 +1,20 @@ group('integration')->in('Visual'); + +$globalHook = (object) []; // NOTE: global test value container to be mutated and checked across files, as needed + +uses() + ->beforeEach(function () { + $this->baz = 0; + }) + ->beforeAll(function () use ($globalHook) { + $globalHook->beforeAll = 0; + }) + ->afterEach(function () { + $this->ith = 0; + }) + ->afterAll(function () use ($globalHook) { + $globalHook->afterAll = 0; + }) + ->in('Hooks');