Merge pull request #282 from jordanbrauer/reusable-hooks

Reusable hooks
This commit is contained in:
Nuno Maduro
2021-05-02 19:51:59 +01:00
committed by GitHub
11 changed files with 342 additions and 19 deletions

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Pest\Concerns; namespace Pest\Concerns;
use Closure; use Closure;
use Pest\Support\ChainableClosure;
use Pest\Support\ExceptionTrace; use Pest\Support\ExceptionTrace;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\ExecutionOrderDependency; use PHPUnit\Framework\ExecutionOrderDependency;
@ -34,6 +35,38 @@ trait TestCase
*/ */
private $__test; 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. * Creates a new instance of the test case.
*/ */
@ -73,6 +106,68 @@ trait TestCase
$this->setDependencies($tests); $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 * Returns the test case name. Note that, in Pest
* we ignore withDataset argument as the description * we ignore withDataset argument as the description
@ -97,6 +192,10 @@ trait TestCase
$beforeAll = TestSuite::getInstance()->beforeAll->get(self::$__filename); $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)); call_user_func(Closure::bind($beforeAll, null, self::class));
} }
@ -107,6 +206,10 @@ trait TestCase
{ {
$afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename); $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)); call_user_func(Closure::bind($afterAll, null, self::class));
parent::tearDownAfterClass(); parent::tearDownAfterClass();
@ -123,6 +226,10 @@ trait TestCase
$beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename); $beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename);
if ($this->beforeEach instanceof Closure) {
$beforeEach = ChainableClosure::from($this->beforeEach, $beforeEach);
}
$this->__callClosure($beforeEach, func_get_args()); $this->__callClosure($beforeEach, func_get_args());
} }
@ -133,6 +240,10 @@ trait TestCase
{ {
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename); $afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
if ($this->afterEach instanceof Closure) {
$afterEach = ChainableClosure::from($this->afterEach, $afterEach);
}
$this->__callClosure($afterEach, func_get_args()); $this->__callClosure($afterEach, func_get_args());
parent::tearDown(); parent::tearDown();

View File

@ -90,8 +90,6 @@ final class Command extends BaseCommand
*/ */
$this->arguments = AddsDefaults::to($this->arguments); $this->arguments = AddsDefaults::to($this->arguments);
LoadStructure::in($this->testSuite->rootPath);
$testRunner = new TestRunner($this->arguments['loader']); $testRunner = new TestRunner($this->arguments['loader']);
$testSuite = $this->arguments['test']; $testSuite = $this->arguments['test'];
@ -127,6 +125,8 @@ final class Command extends BaseCommand
*/ */
public function run(array $argv, bool $exit = true): int public function run(array $argv, bool $exit = true): int
{ {
LoadStructure::in($this->testSuite->rootPath);
$result = parent::run($argv, false); $result = parent::run($argv, false);
/* /*

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest\PendingObjects; namespace Pest\PendingObjects;
use Closure;
use Pest\Exceptions\InvalidUsesPath; use Pest\Exceptions\InvalidUsesPath;
use Pest\TestSuite; use Pest\TestSuite;
@ -12,6 +13,20 @@ use Pest\TestSuite;
*/ */
final class UsesCall 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<int, Closure>
*/
private $hooks = [];
/** /**
* Holds the class and traits. * Holds the class and traits.
* *
@ -97,11 +112,56 @@ final class UsesCall
return $this; 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. * Dispatch the creation of uses.
*/ */
public function __destruct() 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,
);
} }
} }

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest\Repositories; namespace Pest\Repositories;
use Closure;
use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\ShouldNotHappen;
use Pest\Exceptions\TestAlreadyExist; use Pest\Exceptions\TestAlreadyExist;
use Pest\Exceptions\TestCaseAlreadyInUse; use Pest\Exceptions\TestCaseAlreadyInUse;
@ -24,7 +25,7 @@ final class TestRepository
private $state = []; private $state = [];
/** /**
* @var array<string, array<int, array<int, string>>> * @var array<string, array<int, array<int, string|Closure>>>
*/ */
private $uses = []; private $uses = [];
@ -46,12 +47,13 @@ final class TestRepository
}; };
foreach ($this->uses as $path => $uses) { foreach ($this->uses as $path => $uses) {
[$classOrTraits, $groups] = $uses; [$classOrTraits, $groups, $hooks] = $uses;
$setClassName = function (TestCaseFactory $testCase, string $key) use ($path, $classOrTraits, $groups, $startsWith): void {
$setClassName = function (TestCaseFactory $testCase, string $key) use ($path, $classOrTraits, $groups, $startsWith, $hooks): void {
[$filename] = explode('@', $key); [$filename] = explode('@', $key);
if ((!is_dir($path) && $filename === $path) || (is_dir($path) && $startsWith($filename, $path))) { 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 (class_exists($class)) {
if ($testCase->class !== TestCase::class) { if ($testCase->class !== TestCase::class) {
throw new TestCaseAlreadyInUse($testCase->class, $class, $filename); throw new TestCaseAlreadyInUse($testCase->class, $class, $filename);
@ -62,10 +64,12 @@ final class TestRepository
} }
} }
$testCase // IDEA: Consider set the real lines on these.
->factoryProxies $testCase->factoryProxies->add($filename, 0, 'addGroups', [$groups]);
// Consider set the real line here. $testCase->factoryProxies->add($filename, 0, 'addBeforeAll', [$hooks[0] ?? null]);
->add($filename, 0, 'addGroups', [$groups]); $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; $state = count($onlyState) > 0 ? $onlyState : $this->state;
foreach ($state as $testFactory) { foreach ($state as $testFactory) {
/* @var TestCaseFactory $testFactory */ /** @var TestCaseFactory $testFactory */
$tests = $testFactory->build($testSuite); $tests = $testFactory->build($testSuite);
foreach ($tests as $test) { foreach ($tests as $test) {
$each($test); $each($test);
@ -92,11 +96,12 @@ final class TestRepository
/** /**
* Uses the given `$testCaseClass` on the given `$paths`. * Uses the given `$testCaseClass` on the given `$paths`.
* *
* @param array<int, string> $classOrTraits * @param array<int, string> $classOrTraits
* @param array<int, string> $groups * @param array<int, string> $groups
* @param array<int, string> $paths * @param array<int, string> $paths
* @param array<int, Closure> $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) { foreach ($classOrTraits as $classOrTrait) {
if (!class_exists($classOrTrait) && !trait_exists($classOrTrait)) { if (!class_exists($classOrTrait) && !trait_exists($classOrTrait)) {
@ -109,9 +114,10 @@ final class TestRepository
$this->uses[$path] = [ $this->uses[$path] = [
array_merge($this->uses[$path][0], $classOrTraits), array_merge($this->uses[$path][0], $classOrTraits),
array_merge($this->uses[$path][1], $groups), array_merge($this->uses[$path][1], $groups),
$this->uses[$path][2] + $hooks, // NOTE: array_merge will destroy numeric indices
]; ];
} else { } else {
$this->uses[$path] = [$classOrTraits, $groups]; $this->uses[$path] = [$classOrTraits, $groups, $hooks];
} }
} }
} }

View File

@ -12,7 +12,7 @@ use Closure;
final class ChainableClosure 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 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_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());
};
}
} }

View File

@ -103,6 +103,18 @@
PASS Tests\Fixtures\ExampleTest PASS Tests\Fixtures\ExampleTest
✓ it example 2 ✓ 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 PASS Tests\PHPUnit\CustomAffixes\InvalidTestName
✓ it runs file names like `@#$%^&()-_=+.php` ✓ it runs file names like `@#$%^&()-_=+.php`
@ -209,5 +221,5 @@
✓ it is a test ✓ it is a test
✓ it uses correct parent class ✓ it uses correct parent class
Tests: 7 skipped, 115 passed Tests: 7 skipped, 119 passed

View File

@ -0,0 +1,27 @@
<?php
global $globalHook;
uses()->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');
});

View File

@ -0,0 +1,23 @@
<?php
uses()->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');
});

View File

@ -0,0 +1,28 @@
<?php
global $globalHook;
uses()->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);
});

View File

@ -0,0 +1,26 @@
<?php
uses()->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);
});

View File

@ -1,3 +1,20 @@
<?php <?php
uses()->group('integration')->in('Visual'); uses()->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');