Blog / Access Laravel before and after running Pest tests


11 min - 18 Nov 2024
TL;DR: How to access the Laravel ecosystem by simulating the beforeAll and afterAll methods in a Pest test.
A sample Laravel project can be found on this Github Repository. Find out more on Capsules, X or Bluesky.
I needed a metaphor to illustrate my article and characterize a Pest test.
Claude: A Pest test is like a hamburger. The core of the test is the juicy patty: it’s the main part, the one that brings all the flavor. Around it, you have the beforeEach and afterEach functions, which act as the toppings and sauces, adding flavor and context to each bite. Finally, the beforeAll and afterAll functions are like the buns: they provide structure and hold everything together. Together, it all creates a perfectly balanced testing experience. In short, writing a Pest test is like making a good burger: every ingredient matters, and it’s their harmony that makes all the difference.
Hmm. Okay. Thanks, Claude.
According to the PestPHP documentation, this $this variable is not accessible in the beforeAll and afterAll methods of a test. This is because these hooks are executed before any test is run.
beforeAll()
Executes the provided closure once before any tests are run within the current file, allowing you to perform any necessary setup or initialization that applies to all tests.
beforeAll(function () {
// Prepare something once before any of this file's tests run...
});
It's important to note that unlike the beforeEach() hook, the $this variable is not available in the beforeAll() hook. This is because the hook runs before any tests are executed, so there is no instance of the test class or object to which the variable could refer.
To understand this better, it is important to note that Pest is built on PHPUnit. Pest's beforeAll and afterAll functions are based on PHPUnit's setUp and tearDown methods, which execute once per test. However, in the case of Pest, these hooks are only called once per file. This is the magic of Pest. The only issue is that nothing is accessible during these calls.
It is therefore impossible to access $this, the Laravel application, or its properties within a beforeAll() or afterAll() hook. At least not without a specific workaround. This article explores that workaround.
From a standard Laravel project, replace PHPUnit with Pest.
composer remove --dev phpunit/phpunit
composer require --dev pestphp/pest --with-all-dependencies
vendor/bin/pest --init
Run the tests with vendor/bin/pest :
> vendor/bin/pest
PASS Tests\Unit\ExampleTest
✓ that true is true
PASS Tests\Feature\ExampleTest
✓ the application returns a successful response
Tests: 2 passed (2 assertions)
Duration: 0.21s
If the project uses Vite, a Vite manifest not found error may occur. To resolve this issue, Vite needs to be disabled in the setUp method of the TestCase.php file.
tests/TestCase.php
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
protected function setUp() : void
{
parent::setUp();
$this->withoutVite();
}
}
For the purposes of this article, the Feature directory is removed. The Feature testsuite must then be deleted from the phpunit.xml file, along with the corresponding line in the Pest.php file. Finally, the ExampleTest.php file is replaced with BootloadableTest.php.
phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
</phpunit>
tests/Pest.php
<?php
use Tests\TestCase;
pest()->extend( TestCase::class )->in( 'Unit' );
tests/Unit/BootloadableTest.php
<?php
it( "can say 'Hello World!'", function()
{
$message = 'Hello World!';
echo "$message\n";
expect( $message )->toBeString()->toEqual( 'Hello World !' );
} );
> vendor/bin/pest
Hello World!
PASS Tests\Unit\BootloadableTest
✓ it can say 'Hello World!'
Tests: 1 passed (2 assertions)
Duration: 0.07s
The goal of this article is to share the same data across tests, generated from a single execution of the command php artisan migrate --seed.
To do this, it is necessary to slightly modify the DatabaseSeeder.php file to create two User per seed.
database/seeders/DatabaseSeeder.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
class DatabaseSeeder extends Seeder
{
public function run() : void
{
User::factory( 2 )->create();
}
}
At the start of one test.
tests/Unit/BootloadableTest.php
<?php
use App\Models\User;
it( "can dump users", function()
{
$this->artisan( 'migrate:fresh --seed' );
dd( User::select( 'name', 'email' )->get()->toArray() );
} );
> vendor/bin/pest
array:2 [
0 => array:2 [
"name" => "Evie Cronin"
"email" => "ebert.paolo@example.org"
]
1 => array:2 [
"name" => "Heidi Dietrich"
"email" => "orempel@example.net"
]
] // tests/Unit/BootloadableTest.php:10
At the start of two identical tests, by adding the Artisan command in the beforeEach() hook.
tests/Unit/BootloadableTest.php
<?php
use App\Models\User;
beforeEach( function()
{
$this->artisan( 'migrate:fresh --seed' );
} );
it( "can dump users for the first time", function()
{
dump( User::select( 'name', 'email' )->get()->toArray() );
} );
it( "can dump users for the second time", function()
{
dd( User::select( 'name', 'email' )->get()->toArray() );
} );
> vendor/bin/pest
array:2 [
0 => array:2 [
"name" => "Mr. Elmer Jerde I"
"email" => "hector.okuneva@example.net"
]
1 => array:2 [
"name" => "Prof. Rupert Toy"
"email" => "bosco.ferne@example.com"
]
] // tests/Unit/BootloadableTest.php:14
array:2 [
0 => array:2 [
"name" => "Nona Howe MD"
"email" => "zulauf.jarred@example.com"
]
1 => array:2 [
"name" => "Abbie O'Conner"
"email" => "lhowell@example.net"
]
] // tests/Unit/BootloadableTest.php:19
The result is predictable, as beforeEach, as its name suggests, executes before each test. In this case, why not use the beforeAll hook instead?
tests/Unit/BootloadableTest.php on line 6
...
beforeAll( function()
{
$this->artisan( 'migrate:fresh --seed' );
} );
...
> vendor/bin/pest
FAIL Tests\Unit\BootloadableTest
─────────────────────────────────────────
FAILED Tests\Unit\BootloadableTest >
Using $this when not in object context
The $this variable is not accessible in the beforeAll function, nor are the facades. For example, Artisan::call('migrate:fresh --seed') does not work in this context either. The alternative : use the Bootloadable trait.
The trait overrides the setUp and tearDown functions to add two new methods : initialize and finalize. The initialize method runs once, like beforeAll, while the finalize method also runs once, like afterAll.
tests\Traits\Bootloadable.php
<?php
namespace Tests\Traits;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Pest\TestSuite;
trait Bootloadable
{
private static int $count = 0;
private static Collection $tests;
protected function setUp() : void
{
parent::setUp();
if( ! self::$count )
{
$this->init();
if( method_exists( self::class, 'initialize' ) ) $this->initialize();
}
self::$count++;
}
protected function tearDown() : void
{
if( count( self::$tests ) == self::$count )
{
if( method_exists( self::class, 'finalize' ) ) $this->finalize();
}
parent::tearDown();
}
private function init() : void
{
$repository = TestSuite::getInstance()->tests;
$data = [];
foreach( $repository->getFilenames() as $file )
{
$factory = $repository->get( $file );
$filename = Str::of( $file )->basename()->explode( '.' )->first();
if( Str::contains( self::class, $factory->class ) ) $data = [ ...$data, ...[ $filename => $factory->methods ] ];
}
$cases = Collection::make( Arr::dot( $data ) );
$only = $cases->filter( fn( $case ) => Collection::make( $case->groups )->contains( '__pest_only' ) );
self::$tests = ( $only->isEmpty() ? $cases : $only )->keys()->map( fn( $key ) => Str::of( $key )->kebab );
}
}
The setUp and tearDown methods are limited to calling initialize and finalize. The core logic resides in the init method.
This method lists the number of tests. The initialize method is called when the initial count is 0, while incrementing the static variable $count. The finalize method is called when this count reaches the length of the $tests array, initialized by the init function.
To use the initialize method, you need to add the trait to the TestCase and implement the corresponding method.
tests/TestCase.php
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Tests\Traits\Bootloadable;
abstract class TestCase extends BaseTestCase
{
use Bootloadable;
protected function initialize() : void
{
$this->artisan( 'migrate:fresh --seed' );
}
}
initialize method can thus replace the setUp method. In the case of $this->withoutVite, this configuration can be directly added to initialize.Don’t forget to remove the previous beforeAll() from the BootloadableTest file. Then, running vendor/bin/pest twice will yield the following result : two different migrations and seeds for the same test suite.
> vendor/bin/pest
array:2 [
0 => array:2 [
"name" => "Prof. Moises Skiles IV"
"email" => "frederique.nitzsche@example.com"
]
1 => array:2 [
"name" => "Prof. Cleve Oberbrunner"
"email" => "madelynn.hane@example.net"
]
] // tests/Unit/BootloadableTest.php:8
array:2 [
0 => array:2 [
"name" => "Prof. Moises Skiles IV"
"email" => "frederique.nitzsche@example.com"
]
1 => array:2 [
"name" => "Prof. Cleve Oberbrunner"
"email" => "madelynn.hane@example.net"
]
] // tests/Unit/BootloadableTest.php:13
> vendor/bin/pest
array:2 [
0 => array:2 [
"name" => "Doug Marvin"
"email" => "nash.schoen@example.com"
]
1 => array:2 [
"name" => "Joaquin Jacobi"
"email" => "neoma38@example.net"
]
] // tests/Unit/BootloadableTest.php:8
array:2 [
0 => array:2 [
"name" => "Doug Marvin"
"email" => "nash.schoen@example.com"
]
1 => array:2 [
"name" => "Joaquin Jacobi"
"email" => "neoma38@example.net"
]
] // tests/Unit/BootloadableTest.php:13
It is now time to manipulate the data across the different tests.
Let’s imagine that the first test modifies the name of the first user, and the second test verifies the updated name from the first test.
Tests/Unit/BootloadableTest.php
<?php
use App\Models\User;
beforeEach( function()
{
$this->user = User::first();
$this->new = "Capsules Codes";
} );
it( "can modify first user name between two tests", function()
{
$name = $this->user->name;
echo $this->user->name;
expect( $this->user->name )->toBe( $name );
$this->user->name = $this->new;
$this->user->save();
} );
it( "can verify first user name between two tests", function()
{
echo " > {$this->user->name} \n";
expect( $this->user->name )->toBe( $this->new );
} );
The result after two consecutive executions of the vendor/bin/pest command.
> vendor/bin/pest
Esteban Raynor > Capsules Codes
PASS Tests\Unit\BootloadableTest
✓ it can modify first user name between two tests
✓ it can verify first user name between two tests
Tests: 2 passed (2 assertions)
Duration: 0.40s
> vendor/bin/pest
Demario Corkery > Capsules Codes
PASS Tests\Unit\BootloadableTest
✓ it can modify first user name between two tests
✓ it can verify first user name between two tests
Tests: 2 passed (2 assertions)
Duration: 0.39s
What happens if each test is placed in its own file?
Unit/FirstTest.php
<?php
use App\Models\User;
beforeEach( function()
{
$this->user = User::first();
$this->new = "Capsules Codes";
} );
it( "can modify first user name between two tests", function()
{
$name = $this->user->name;
echo "{$this->user->name} > $this->new \n";
expect( $this->user->name )->toBe( $name );
$this->user->name = $this->new;
$this->user->save();
} );
Unit/SecondTest.php
<?php
use App\Models\User;
beforeEach( function()
{
$this->user = User::first();
$this->new = "Capsules Codes";
} );
it( "can verify first user name between two tests", function()
{
expect( $this->user->name )->toBe( $this->new );
} );
vendor/bin/pest
Mrs. Laurine Ebert V > Capsules Codes
PASS Tests\Unit\FirstTest
✓ it can modify first user name between two test files 0.08s
PASS Tests\Unit\SecondTest
✓ it can verify first user name between two test files 0.01s
Tests: 2 passed (2 assertions)
Duration: 0.13s
Unlike Pest, which executes the beforeAll and afterAll methods at the start and end of a file's tests, here it operates on a per-TestCase basis rather than per file. To execute the initialize and finalize methods at the beginning and end of the tests for a given file, a slight modification to the Trait is necessary.
tests/Traits/Bootloadable.php
<?php
namespace Tests\Traits;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Pest\TestSuite;
trait Bootloadable
{
private static int $count = 0;
private static array $tests;
private static string $current;
protected function setUp() : void
{
parent::setUp();
self::$current = array_reverse( explode( '\\', debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 )[ 1 ][ 'class' ] ) )[ 0 ];
if( ! isset( self::$tests ) )
{
$this->init();
}
if( ! self::$count )
{
if( method_exists( self::class, 'initialize' ) ) $this->initialize( self::$current );
}
self::$count++;
}
protected function tearDown() : void
{
if( self::$tests[ self::$current ] == self::$count )
{
if( method_exists( self::class, 'finalize' ) ) $this->finalize( self::$current );
self::$count = 0;
}
parent::tearDown();
}
private function init() : void
{
$repository = TestSuite::getInstance()->tests;
$data = [];
foreach( $repository->getFilenames() as $file )
{
$factory = $repository->get( $file );
$filename = Str::of( $file )->basename()->explode( '.' )->first();
if( Str::contains( self::class, $factory->class ) ) $data = [ ...$data, ...[ $filename => count( $factory->methods ) ] ];
}
self::$tests = $data;
}
}
debug_backtrace function.self::$tests is now an associative array, where each key represents the filename, and each value corresponds to the number of tests it contains.self::$count is reset to zero once the finalize method has been executed.Last step : modify the TestCase file. This will allow access to the filename within it.
tests/TestCase.php
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Tests\Traits\Bootloadable;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\File;
abstract class TestCase extends BaseTestCase
{
use Bootloadable;
protected function initialize( $filename ) : void
{
if( $filename == "FirstTest" )
{
$this->artisan( 'migrate:fresh --seed' );
}
if( $filename == "SecondTest" )
{
$this->artisan( 'migrate:fresh --seed' );
}
}
protected function finalize( $filename ) : void
{
if( $filename == "FirstTest" )
{
$this->artisan( 'migrate:reset' );
}
if( $filename == "SecondTest" )
{
$this->artisan( 'migrate:reset' );
}
}
}
The tests can ben be re-run.
> vendor/bin/test
Brice Fisher > Capsules Codes
PASS Tests\Unit\FirstTest
✓ it can modify first user name between two tests 0.36s
FAIL Tests\Unit\SecondTest
⨯ it can verify first user name between two tests 0.03s
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
FAILED Tests\Unit\SecondTest > it can verify first user name between two tests
Failed asserting that two strings are identical.
-'Capsules Codes'
+'Tremayne Spinka'
at tests/Unit/SecondTest.php:18
14▕
15▕
16▕ it( "can verify first user name between two tests", function()
17▕ {
➜ 18▕ expect( $this->user->name )->toBe( $this->new );
19▕ } );
20▕
1 tests/Unit/SecondTest.php:18
Tests: 1 failed, 1 passed (2 assertions)
Duration: 0.43s
It seems that Brice Fisher has become Tremayne Spinka!
Here is an overview of the result obtained by duplicating the BootloadableTest file into two distinct files. [ Make sure to replace "Capsules Codes" with "Pest PHP" in the second file ].
> vendor/bin/pest
Ernestine Dietrich III > Capsules Codes
PASS Tests\Unit\FirstTest
✓ it can modify first user name between two test files
✓ it can verify first user name between two test files
Dr. Nya Gusikowski > Pest PHP
PASS Tests\Unit\SecondTest
✓ it can modify first user name between two test files
✓ it can verify first user name between two test files
Tests: 4 passed (4 assertions)
Duration: 0.42s
Glad this helped.