Back to Course |
Re-creating Booking.com API with Laravel and PHPUnit

Using More Pest Features

Once we are familiar with the Pest tests given to us by Shift, we can look into improving them and using more Pest features. Our goal here is to make our tests shorter, and easier to read/understand. We will do this by using the following techniques:

  • Using Helpers
  • Removing $this-> where possible
  • Chaining assertions

Introducing Helper Functions

On the first look at our new tests, we can see that we have a lot of repetition. We are constantly creating users:

$owner = User::factory()->owner()->create();
 
// or
 
$user = User::factory()->user()->create();

This can be improved by using a helper function:

tests/Pest.php

function createOwner(): User
{
return User::factory()->owner()->create();
}
 
function createUser(): User
{
return User::factory()->user()->create();
}

Now we can use these functions in our tests:

tests/Feature/ApartmentShowTest.php

test('apartment show loads apartment with facilities', function () {
// Instead of:
// $owner = User::factory()->owner()->create();
 
$owner = createOwner();
});

tests/Feature/BookingsTest.php

test('user can get only their bookings', function () {
// Instead of:
// $user1 = User::factory()->user()->create();
// $user2 = User::factory()->user()->create();
 
$user1 = createUser();
$user2 = createUser();
});
 
test('user can book apartment successfully but not twice', function () {
// Instead of:
// $user = User::factory()->user()->create();
 
$user = createUser();
});
 
test('user can cancel their booking but still view it', function () {
// $user1 = User::factory()->user()->create();
// $user2 = User::factory()->user()->create();
 
$user1 = createUser();
$user2 = createUser();
});
 
test('user can post rating for their booking', function () {
// $user1 = User::factory()->user()->create();
// $user2 = User::factory()->user()->create();
 
$user1 = createUser();
$user2 = createUser();
});

And so on. You can see the full list of changes in Repository

This is one example of how Pest takes away the logic into a separate layer (helpers, in this case) and makes the main test shorter.


Helper that Uses Another Helper

The next thing we see is repeated code for API calls:

$owner = createOwner();
$response = $this->actingAs($owner);
// Or
$user = createUser();
$response = $this->actingAs($user);

Wouldn't it be nice to simplify this? We can, with another helper!

tests/Pest.php

function asOwner()
{
return test()->actingAs(createOwner());
}
 
function asUser()
{
return test()->actingAs(createUser());
}

This helper will replace:

$owner = User::factory()->owner()->create();
$response = $this->actingAs($owner)->getJson('/api/owner/properties');
 
// With ONE line:
 
$response = asOwner()->getJson('/api/owner/properties');

From Repeating $response To Chaining

Pest allows us to not repeat the operations with the same $response variable, we will chain our response and assertions for cleaner code:

tests/Feature/BookingsTest.php

 
test('user can get only their bookings', function () {
// Instead of:
// $response = $this->actingAs($user1)->getJson('/api/user/bookings');
// $response->assertStatus(200);
// $response->assertJsonCount(1);
// $response->assertJsonFragment(['guests_adults' => 1]);
 
actingAs($user1)
->getJson('/api/user/bookings')
->assertStatus(200)
->assertJsonCount(1)
->assertJsonFragment(['guests_adults' => 1]);
 
// Instead of:
// $response = $this->actingAs($user1)->getJson('/api/user/bookings/' . $booking1->id);
// $response->assertStatus(200);
// $response->assertJsonFragment(['guests_adults' => 1]);
 
actingAs($user1)
->getJson('/api/user/bookings/' . $booking1->id)
->assertStatus(200)
->assertJsonFragment(['guests_adults' => 1]);
 
// $response = $this->actingAs($user1)->getJson('/api/user/bookings/' . $booking2->id);
// $response->assertStatus(403);
 
actingAs($user1)->getJson('/api/user/bookings/' . $booking2->id)
->assertStatus(403);
});
 
test('property owner does not have access to bookings feature', function () {
asOwner()->getJson('/api/user/bookings')->assertStatus(403);
});

Let's perform the same improvement in other test files.

tests/Feature/PropertiesTest.php

test('property owner has access to properties feature', function () {
// $owner = User::factory()->owner()->create();
// $response = $this->actingAs($owner)->getJson('/api/owner/properties');
// $response->assertStatus(200);
asOwner()->getJson('/api/owner/properties')->assertStatus(200);
});
 
test('user does not have access to properties feature', function () {
// $user = User::factory()->user()->create();
// $response = $this->actingAs($user)->getJson('/api/owner/properties');
// $response->assertStatus(403);
asUser()->getJson('/api/owner/properties')->assertStatus(403);
});
 
test('property owner can add property', function () {
// $owner = User::factory()->owner()->create();
// $response = $this->actingAs($owner)->postJson('/api/owner/properties', [
// 'name' => 'My property',
// 'city_id' => City::value('id'),
// 'address_street' => 'Street Address 1',
// 'address_postcode' => '12345',
// ]);
// $response->assertSuccessful();
// $response->assertJsonFragment(['name' => 'My property']);
 
asOwner()
->postJson('/api/owner/properties', [
'name' => 'My property',
'city_id' => City::value('id'),
'address_street' => 'Street Address 1',
'address_postcode' => '12345',
])
->assertSuccessful()
->assertJsonFragment(['name' => 'My property']);
});
 
test('property owner can add photo to property', function () {
// $response = $this->actingAs($owner)->postJson('/api/owner/properties/' . $property->id . '/photos', [
// 'photo' => UploadedFile::fake()->image('photo.png')
// ]);
// $response->assertStatus(200);
// $response->assertJsonFragment([
// 'filename' => config('app.url') . '/storage/1/photo.png',
// 'thumbnail' => config('app.url') . '/storage/1/conversions/photo-thumbnail.jpg',
// ]);
actingAs($owner)
->postJson('/api/owner/properties/' . $property->id . '/photos', [
'photo' => UploadedFile::fake()->image('photo.png')
])
->assertStatus(200)
->assertJsonFragment([
'filename' => config('app.url') . '/storage/1/photo.png',
'thumbnail' => config('app.url') . '/storage/1/conversions/photo-thumbnail.jpg',
]);
});

And so on. You can see the full list of changes in Repository


Importing Pest Functions

Next, in Pest, you can use a lot of well-known functions from PHPUnit: getJson(), postJson(), actingAs(), and so on. You just have to import them upfront, and then you don't have to use $this-> all the time.

tests/Feature/AuthTest.php

use function Pest\Laravel\{postJson};
 
test('registration fails with admin role', function () {
// Instead of:
// $this->postJson('/api/auth/register', [...]);
 
postJson('/api/auth/register', [
'name' => 'Valid name',
'email' => 'valid@email.com',
'password' => 'ValidPassword',
'password_confirmation' => 'ValidPassword',
'role_id' => Role::ROLE_ADMINISTRATOR
])
->assertStatus(422);
});
test('registration succeeds with owner role', function () {
// Instead of:
// $this->postJson('/api/auth/register', [...]);
 
postJson('/api/auth/register', [
'name' => 'Valid name',
'email' => 'valid@email.com',
'password' => 'ValidPassword',
'password_confirmation' => 'ValidPassword',
'role_id' => Role::ROLE_OWNER
])
->assertStatus(200)
->assertJsonStructure([
'access_token',
]);
});
test('registration succeeds with user role', function () {
// Instead of:
// $this->postJson('/api/auth/register', [...]);
 
postJson('/api/auth/register', [
'name' => 'Valid name',
'email' => 'valid@email.com',
'password' => 'ValidPassword',
'password_confirmation' => 'ValidPassword',
'role_id' => Role::ROLE_USER
])
->assertStatus(200)
->assertJsonStructure([
'access_token',
]);
});

Personally, I don't really like the indentation of ->assertStatus() that starts to the right of the main postJson(), but that's how PhpStorm auto-formatted this code. I guess, it's a personal preference, at the end of the day.

Now, you can even import multiple functions like that:

tests/Feature/PropertiesTest.php

use function Pest\Laravel\{actingAs, assertDatabaseHas};
 
test('property owner can reorder photos in property', function () {
// ...
 
actingAs($owner)
->postJson('/api/owner/properties/' . $property->id . '/photos/1/reorder/' . $newPosition)
->assertStatus(200)
->assertJsonFragment(['newPosition' => $newPosition]);
 
// Imported function `assertDatabaseHas`
assertDatabaseHas('media', ['file_name' => $photo1->file_name, 'position' => $newPosition]);
assertDatabaseHas('media', ['file_name' => $photo2->file_name, 'position' => $photo1->position]);
});

All those small changes give us a unified Pest-style code. In general, as you can see, one of the things that Pest does is separate some global settings into helpers/functions and shorten the main test code. Some people don't see it as a big deal, so it's a personal preference whether to use PHPUnit or Pest.

You can find all the changes above in the Repository Pull Request.