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

Tests "Cleanup" with Factories

While reviewing our tests, I decided we can improve things a bit, by using factories more to have more reusable and shorter code in tests. Let's do exactly that, in this short lesson.

Example:

// Current
$owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
 
// Replacing with Factory States
$owner = User::factory()->owner()->create();

Using Factories Instead of Hardcoded Data

We have our User factory defined with multiple states:

database/factories/UserFactory.php

class UserFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
];
}
 
/**
* Indicate that the model's email address should be unverified.
*
* @return static
*/
public function unverified()
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
 
public function owner()
{
return $this->state(fn(array $attributes) => [
'role_id' => Role::ROLE_OWNER,
]);
}
 
public function user()
{
return $this->state(fn(array $attributes) => [
'role_id' => Role::ROLE_USER,
]);
}
 
}

As you can see, we have owner() and user() states defined, yet we are still using hardcoded data in our tests. Let's change that.

tests/Feature/ApartmentShowTest.php

public function test_apartment_show_loads_apartment_with_facilities()
{
// Replace this:
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
 
// With this:
$owner = User::factory()->owner()->create();
}

Let's do exactly that, in all our test methods.

tests/Feature/BookingsTest.php

// ...
private function create_apartment(): Apartment
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}
 
public function test_user_can_get_only_their_bookings()
{
// $user1 = User::factory()->create(['role_id' => Role::ROLE_USER]);
// $user2 = User::factory()->create(['role_id' => Role::ROLE_USER]);
$user1 = User::factory()->user()->create();
$user2 = User::factory()->user()->create();
// ...
}
 
public function test_property_owner_does_not_have_access_to_bookings_feature()
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
// ...
}
 
public function test_user_can_book_apartment_successfully_but_not_twice()
{
// $user = User::factory()->create(['role_id' => Role::ROLE_USER]);
$user = User::factory()->user()->create();
}
 
public function test_user_can_cancel_their_booking_but_still_view_it()
{
// $user1 = User::factory()->create(['role_id' => Role::ROLE_USER]);
// $user2 = User::factory()->create(['role_id' => Role::ROLE_USER]);
$user1 = User::factory()->user()->create();
$user2 = User::factory()->user()->create();
}
 
public function test_user_can_post_rating_for_their_booking()
{
// $user1 = User::factory()->create(['role_id' => Role::ROLE_USER]);
// $user2 = User::factory()->create(['role_id' => Role::ROLE_USER]);
$user1 = User::factory()->user()->create();
$user2 = User::factory()->user()->create();
// ...
}
// ...

tests/Feature/PropertiesTest.php

public function test_property_owner_has_access_to_properties_feature()
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}
 
public function test_user_does_not_have_access_to_properties_feature()
{
// $user = User::factory()->create(['role_id' => Role::ROLE_USER]);
$user = User::factory()->user()->create();
}
 
public function test_property_owner_can_add_property()
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}
 
public function test_property_owner_can_add_photo_to_property()
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}
 
public function test_property_owner_can_reorder_photos_in_property()
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}

tests/Feature/PropertySearchTest.php

public function test_property_search_by_city_returns_correct_results(): void
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}
 
public function test_property_search_by_country_returns_correct_results(): void
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}
 
public function test_property_search_by_geoobject_returns_correct_results(): void
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}
 
public function test_property_search_by_capacity_returns_correct_results(): void
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}
 
public function test_property_search_by_capacity_returns_only_suitable_apartments(): void
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}
 
public function test_property_search_beds_list_all_cases(): void
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}
 
public function test_property_search_returns_one_best_apartment_per_property()
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}
 
public function test_property_search_filters_by_facilities()
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}
 
public function test_property_search_filters_by_price()
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}
 
public function test_properties_show_correct_rating_and_ordered_by_it()
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
 
// $user1 = User::factory()->create(['role_id' => Role::ROLE_USER]);
// $user2 = User::factory()->create(['role_id' => Role::ROLE_USER]);
$user1 = User::factory()->user()->create();
$user2 = User::factory()->user()->create();
}
 
public function test_search_shows_only_apartments_available_for_dates()
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
 
// $user1 = User::factory()->create(['role_id' => Role::ROLE_USER]);
$user1 = User::factory()->user()->create();
}

tests/Feature/PropertyShowTest.php

// ...
public function test_property_show_loads_property_correctly()
{
// $owner = User::factory()->create(['role_id' => Role::ROLE_OWNER]);
$owner = User::factory()->owner()->create();
}

This multi-replace removes a lot of hard-coded values and makes our tests more readable and easier to maintain. If the condition on how we define a user or owner changes, we only need to change it in one place - in the Factory.


Using Factories with Files

One of the things that were left out in previous lessons was how files were uploaded for the test_property_owner_can_reorder_photos_in_property test:

tests/Feature/PropertiesTest.php

// ...
public function test_property_owner_can_reorder_photos_in_property()
{
// ...
 
// I admit I'm lazy here: 2 API calls to upload files, instead of building a factory
$photo1 = $this->actingAs($owner)->postJson('/api/owner/properties/' . $property->id . '/photos', [
'photo' => UploadedFile::fake()->image('photo1.png')
]);
$photo2 = $this->actingAs($owner)->postJson('/api/owner/properties/' . $property->id . '/photos', [
'photo' => UploadedFile::fake()->image('photo2.png')
]);
 
$newPosition = $photo1->json('position') + 1;
$response = $this->actingAs($owner)->postJson('/api/owner/properties/' . $property->id . '/photos/1/reorder/' . $newPosition);
$response->assertStatus(200);
$response->assertJsonFragment(['newPosition' => $newPosition]);
 
$this->assertDatabaseHas('media', ['file_name' => 'photo1.png', 'position' => $photo2->json('position')]);
$this->assertDatabaseHas('media', ['file_name' => 'photo2.png', 'position' => $photo1->json('position')]);
}
// ...

While this works, we can do better. We can use the factory state to create fake files for us:

database/factories/PropertyFactory.php

class PropertyFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->text(20),
'address_street' => fake()->streetAddress(),
'address_postcode' => fake()->postcode(),
'lat' => fake()->latitude(),
'long' => fake()->longitude(),
];
}
 
public function withImages($count = 2)
{
return $this->afterCreating(function ($property) use ($count) {
for ($i = 0; $i < $count; $i++) {
$property->addMedia(fake()->image())->toMediaCollection('images');
}
});
}
}

This factory state will create a property and add X images to it (2 by default). We can use it in our test:

tests/Feature/PropertiesTest.php

public function test_property_owner_can_reorder_photos_in_property()
{
// ...
 
$property = Property::factory()->withImages()->create([
'owner_id' => $owner->id,
'city_id' => $cityId,
]);
 
$mediaCollection = $property->getMedia('images');
 
$photo1 = $mediaCollection->first();
$photo2 = $mediaCollection->last();
 
$newPosition = $photo1->position + 1;
$response = $this->actingAs($owner)->postJson('/api/owner/properties/' . $property->id . '/photos/1/reorder/' . $newPosition);
$response->assertStatus(200);
$response->assertJsonFragment(['newPosition' => $newPosition]);
 
$this->assertDatabaseHas('media', ['file_name' => $photo1->file_name, 'position' => $newPosition]);
$this->assertDatabaseHas('media', ['file_name' => $photo2->file_name, 'position' => $photo1->position]);
}

With this change, we are not relying on the endpoint to upload our files but rather have a factory state that will do it for us.

This makes our tests more reliable and easier to maintain. Especially if we will have more tests that require files to be uploaded in different amounts.


Summary

In our tests, we should aim to leverage Factories as much as possible. They are great at removing hard-coded values from our tests and making them more maintainable. So while it isn't a huge improvement - we are still setting ourselves up for success in the future.