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:
$this->
where possibleOn 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.
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');
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
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.