Development

Rethinking PHP testing: an Intro to PEST.

4 minutes

Setting the scene: classic PHP testing

Here's an example of a fairly standard test:

php
1use App\Services\Characters;
2use Cache;
3use Tests\TestCase;
4 
5class CharactersTest extends TestCase
6{
7 public function test_will_return_single_character(): void
8 {
9 $characters = $this->app->make(Characters::class);
10 $character = $characters->getCharacter('1009220');
11 $this->assertEquals('Captain America', $character['name']);
12 }
13}

OK, so what's going on here? Well, we've got a snake case test written as a function; we use snake case rather than camel case as our test names tend to get quite... wordy (we like writing lots of tests to cover as much of the codebase as possible!). In the default installation of PHPUnit, it looks for functions starting with test in the tests folder - you can use annotations to be more explicit though. In order to pull in the assertions, we need the PHPUnit base class - in Laravel the abstract TestCase class extends off this. The other important thing to note here is the line

php
1$characters = $this->app->make(Characters::class);

works, because the TestCase bootstraps the Laravel framework up.

Diving deeper into PHPUnit

Let’s take a look at a more complex example of using PHPUnit:

php
1use RefreshDatabase;
2 
3public function test_authorised_user_can_view_empty_dashboard(): void
4{
5 $user = User::factory()->create();
6 
7 $merchant = Merchant::factory()->create();
8 
9 $user->merchants()->attach($merchant->id);
10 
11 $this->actingAs($user);
12 
13 $response = $this->get(route('admin.dashboard'));
14 $response->assertStatus(Response::HTTP_OK);
15 $response->assertSeeText('Dashboard');
16 $response->assertDontSee('My Orders');
17}

So, a few more PHPUnit assertions being used here, some imports and a factory method to create a fixture. It’s pretty common to have a series of tests with a common subject to test such as a fixture, so one way of doing this would be to use PHPUnit’s setUp() method:

php
1protected function setUp(): void
2{
3 $this->user = User::factory()->make();
4 parent::setUp();
5}

We can then access the user property whenever we need in this file of tests. Also the trait being used is important: use RefreshDatabase; means that Laravel will clear the test database between every test. It’s worth noting I’ve changed the fixture to use make() rather than create() for this reason – let’s assume we want to test methods on the model, for example.

The point here is that it all looks very much like standard PHP, and it’s meant to – it makes PHPUnit code easy to read.

And now …Javascript

Our testing at Jump24 doesn’t stop with the backend in PHP. We use the Jest library for unit testing on the frontend (with Cypress for integration testing). Let’s have a look at an example test in Jest, run on a React component:

javascript
1describe('', () => {
2 test('Will display message', async () => {
3 const { getByText } = render(type='success'>Test Message)
4 const message = getByText(/Test Message/)
5 expect(message).toBeInTheDocument()
6 })
7 test('Will use amber background when warning', async () => {
8 const { container } = render(type='warning'>Test Message)
9 expect(container.getElementsByClassName('bg-amber-600').length).toBe(1)
10 })
11})

As you can see, it’s more than just the syntax that is different here – it’s the approach. Javascript naturally gravitates towards closures and more fluid, functional programming. While PHPUnit has introduced parallel tests to speed up test suite runtime execution, the use of async methods in Javascript enables the tests to be truly non-blocking. The advange of the expressive syntax has to be the biggest difference, with assertions reading almost like TDD stories:

php
1test('Example date test', () => {
2 function date () {
3 return Date.now()
4 }
5 // nice and expressive, no?
6 expect(foo()).not.toBe(99999)
7})

How about this…. in PHP?

The Laravel framework has been very gradually introducing more functional coding styles to PHP over the past few years. The way the framework makes use of callbacks and especially how it’s ORM, Eloquent, implements closures within queries is very expressive in a similar fashion to Javascript. Laravel itself styles itself as a web application framework with expressive, elegant syntax, so how about those PHPUnit tests?

There hasn’t really been the equivalent in PHP, until Nuno Madro from the core Laravel team annouced PEST, which takes inspiration from Jest (get it??!!) Let’s look to refactor our PHP test from earlier from one to another.

Basic Jest vs. PEST comparison

Just to start off, we’ll look at the very basic syntax to show the influence on the language, from Javascript to PHP. Firstly, an empty Jest test:

truthtests.js

javascript
1describe('the truth tests', () => {
2 test('this is true', async () => {
3 expect(true).toBeTruthy()
4 })
5 
6 test('this is false', async () => {
7 expect(false).toBeFalsy()
8 })
9})

And now in PEST:

php
1test('is true', fn () => {
2 expect(true)->toBeTrue();
3})
4 
5 
6test('is false', fn () => {
7 expect(false)->toBeFalse();
8})

Pretty close, huh? I mentioned BDD – the syntax even allows us to make it look a bit more like BDD:

php
1is('true', fn () => {
2 expect(true)->toBeTrue();
3})
4 
5false('false', fn () => {
6 expect(false)->toBeFalse();
7})

Enhanced CLI feedback & Coverage

We’ve evolved how we fire tests in PHPUnit over time at Jump24. Originally, we’d add a composer script to fire ./vendor/bin/phpunit –testdox, with a phpunit.xml configuration file read in. Currently we’ve moved to Laravel’s artisan command, with Laravel handling as much of the config as possible (we found test database credentials conflicting between Laravel and phpunit.xml, which was a hard clash to debug!).

PEST’s test runner, just like the artisan command php artisan test comes with the JUnit inspired testdox out of the box.

Laravel Partner

Since 2014, we’ve built, managed and upgraded Laravel applications of all types and scales with clients from across the globe. We are one of Europe’s leading Laravel development agencies and proud to support the framework as an official Laravel Partner.

Vue Experts

We use Vue alongside Laravel on many of our projects. We have grown our team of experts and invested heavily in our knowledge of Vue over the last five years.