PEST Testing
Pest is a Testing Framework with a focus on simplicity. It was carefully crafted to bring the joy of testing to PHP.
Hey there, fellow developers! Let’s be real, testing can be about as fun as watching paint dry. But fear not, because we’ve got a new testing framework in town that’s about to make testing a lot less snooze-worthy. Enter PEST, the cool kid on the testing block that’s making waves in the Laravel community.
Sure, Laravel’s built-in support for PHPUnit testing is reliable and all, but who wants to be stuck in the testing dark ages? PEST offers a more modern and intuitive approach to testing, making it a game-changer for those of us who would rather be doing something else (anything else?) than writing tests.
In this article, we’re going to show you how to get started with PEST testing in Laravel and show by example some of the reasons why you may want to use PEST over PHPUnit. So, grab a coffee (or a stiff drink) and let’s dive into the wild world of PEST testing!
Installation
1) Install Pest via the Composer:
composer require pestphp/pest --dev --with-all-dependencies
2) Install the laravel plugin:
composer require pestphp/pest-plugin-laravel --dev
3) Run the install command via artisan:
php artisan pest:install
Getting Started
Create a feature test:
php artisan pest:test UsersTest
Create a unit test:
php artisan pest:test UsersTest --unit
Compared to PHPUnit tests, there are no classes with namespaces. Rather, they are contained in a function’s closure. The function name is called either ‘it’ or ‘test’.
For example:
test('asserts a status code', function() { $response = $this->get('/'); $response->assertStatus(200); });
Or alternatively:
it('asserts a status code', function() { $response = $this->get('/'); $response->assertStatus(200); });
You may be accustomed to using a ‘setup’ method that all tests in a class can take advantage of. PEST takes an alternative approach. Hooks are available for the various life cycles of a test:
- beforeAll
- beforeEach
- afterEach
- afterAll
It is important to keep in mind that beforeAll and afterAll do not have access to Laravel features (Laravel hasn’t been booted yet).
Example usage:
beforeEach(function() { // Something you want done before each test is run });
Another important difference is how PEST implements RefreshDatabase:
1) You should find it in the Pest.php config and all you’ll need to do is uncomment it:
uses( Tests\TestCase::class, Illuminate\Foundation\Testing\RefreshDatabase::class, )->in('Feature');
2) Alternatively use ‘LazilyRefreshDatabase’, which has the advantage of only refreshing the database when test accesses it. You can update the TestCase.php file like so:
abstract class TestCase extends BaseTestCase { use CreatesApplication; use LazilyRefreshDatabase; }
Custom Helpers
Custom helpers that you would like to have accessible via all tests can be placed in tests/Pest.php. A good example of a helper would be a ‘logiin’ method that creates a user if needed, and returns ‘actingAs’ for the test:
function login($user = null) { return test()->actingAs($user ?? \App\Models\User::factory()->create()); }
You’ll notice the use of test() instead of $this. It’s used to return the current test that the helper is being called from. Keep in mind we’re not using classes here.
Faker
Pest has it’s own implementation of faker. You can find it here.
An example test using our helper login function and faker might look something like:
it('can store a folder', function () { login()->from('/') ->post('/folder', [ 'name' => 'some_file.txt', 'description' => faker()->paragraph(2), 'display' => faker()->randomElement(['visible' ,'hidden']), ]) ->assertRedirect('/') ->assertSessionHas('success', 'Folder created'); $folder = Folder::latest()->first(); expect($folder->name)->toBeString()->toBe('some_file.txt')->not->toBeEmpty() ->and($folder->display->value)->toBeIn(['hidden','visible']) ->and($folder->display->name)->toBeIn(['Hidden','Visible']); });
Pest also has something called ‘Higher Order Expectations‘ that help make your tests more fluent. As a comparison, here is the same test example using higher order expectations:
it('can store a folder', function () { login()->from('/') ->post('/folder', [ 'name' => 'some_file.txt', 'description' => faker()->paragraph(2), 'display' => faker()->randomElement(['visible' ,'hidden']), ]) ->assertRedirect('/') ->assertSessionHas('success', 'Folder created'); expect(Folder::latest()->first()) ->name->toBeString()->toBe('some_file.txt')->not->toBeEmpty() ->display->value->toBeIn(['hidden','visible']) ->display->name->toBeIn(['Hidden','Visible']); });
You may pass an array of data to be tested against. Use the with() method, like so:
it('can store a file while testing with an array data set', function (array $data) { $folder = Folder::factory()->create(); login()->from('/') ->post('/file', [ ...[ 'folder_id' => $folder->id, 'name' => 'some_file.txt', 'description' => faker()->paragraph(2), 'type' => faker()->randomElement(FileType::values()), ], ...$data, ]) ->assertRedirect('/') ->assertSessionHas('success', 'File created'); $file = File::latest()->first(); expect($file) ->description->toBeString()->not->toBeEmpty() ->name->toBeString()->not->toBeEmpty() ->type->value->toBeIn(['image','pdf','text','audio']) ->type->name->toBeIn(['Image','PDF','Text','Audio']); })->with([ 'Default values' => [[]], 'Image file' => [['type' => 'image', 'name' => 'some_other_file.txt']], 'PDF file' => [['type' => 'pdf', 'name' => 'some_other_file.pdf']], ]);
When chaining multiple with() methods, pest will combine them into a cartesian array, looping through each possible combination. Here’s an example using an array set manually as well as an array from an enum:
it('can store a file with while using the pest feature for combined data sets (cartesian arrays)', function($fileResource, $type, $property) { $folder = Folder::factory()->create(); login()->from('/') ->post('/file', [ 'folder_id' => $folder->id, 'name' => $fileResource, 'description' => faker()->paragraph(2), 'type' => $type, 'property' => $property, ]) ->assertRedirect('/') ->assertSessionHas('success', 'File created'); $file = File::latest()->first(); expect($file) ->description->toBeString()->not->toBeEmpty() ->name->toBeString()->not->toBeEmpty() ->property->value->toBeIn(['read only','protected','hidden','visible']) ->property->name->toBeIn(['ReadOnly','Protected','Hidden','Visible']) ->type->value->toBeIn(['image','pdf','text','audio']) ->type->name->toBeIn(['Image','PDF','Text','Audio']); })->with([ ['fileResource' => 'example.gif', 'type' => 'image'], ['fileResource' => 'example.pdf', 'type' => 'pdf'], ['fileResource' => 'example.txt', 'type' => 'text'], ]) ->with(FileProperty::values());
This is only scratching the surface. Testing may not be the most glamorous part of software development, but it’s undoubtedly crucial. Lucky for Laravel developers, PEST testing offers a refreshing and fun approach to testing. Why stick with the same old PHPUnit when you can level up your testing game with PEST?
PEST testing’s expressive and readable syntax is like a breath of fresh air. You won’t get bogged down in confusing boilerplate code. Plus, PEST has a set of testing helpers that will have you testing like a pro in no time. And let’s not forget how seamlessly it integrates with Laravel’s testing infrastructure.
Seriously, who wants to spend all day writing and deciphering tests? With PEST, you can write more robust and stable applications, all while having a little fun. So why not give PEST testing a try? Trust us, your tests (and your sanity) will thank you.
Check back soon for part 2 of of testing with PEST.