It is time to cover restaurant Controllers with tests. Tests help uncover bugs in the code. By running test cases, developers can catch issues early on and make the code base more reliable.
Laravel will automatically set the configuration environment to testing
when running tests. You can create a .env.testing
file at the root of your project. This file will be used instead of the .env
file when running PHPUnit tests or executing Artisan commands with the --env=testing
option.
Tests often involve creating testing datasets in the database we test against, and our case is no exception. Due to this reason, we need to create a separate database for the testing
environment because all entries will get wiped for each case, and we want to preserve our database used in the local
environment.
Copy your .env
file to .env.testing
.
cp .env .env.testing
Create a new database, for example, food_delivery_testing
.
And update the environment file for testing.
.env.testing
# ... DB_DATABASE=food_delivery_testing # ... MAIL_MAILER=log # ...
We updated the DB_DATABASE
value to the testing database we just created.
If you had some mailer set up like mailtrap.io or anything else, change the MAIL_MAILER
value to log
. We do not want to send any emails when running tests.
Include the .env.testing
file into the .gitignore
file because any environment files should not be included in the repository.
.gitignore
.env.production.env.testing .phpunit.result.cache
Let's update UserFactory
and include methods for creating admin and customer users. We will use them in tests.
database/factories/UserFactory.php
public function admin(){ return $this->afterCreating(function (User $user) { $user->roles()->sync(Role::where('name', RoleName::ADMIN->value)->first()); });} // ... public function customer(){ return $this->afterCreating(function (User $user) { $user->roles()->sync(Role::where('name', RoleName::CUSTOMER->value)->first()); });}
Currently, we have DatabaseSeeder
for seeding the database. For testing purposes, it is better to have a separate seeder. We do not need to re-seed 50 users with restaurants for every test.
Create a new TestingSeeder
to seed a single vendor user with restaurants, categories, products, and staff users.
database/seeders/TestingSeeder.php
namespace Database\Seeders; use App\Models\Category;use App\Models\Product;use App\Models\Restaurant;use App\Models\User;use Illuminate\Database\Seeder; class TestingSeeder extends Seeder{ public function run(): void { $this->call([ PermissionSeeder::class, RoleSeeder::class, CitySeeder::class, ]); $this->seedRestaurantAndProduct(); } protected function seedRestaurantAndProduct(): void { $products = Product::factory(); $categories = Category::factory()->has($products); $staffMember = User::factory()->staff(); $restaurant = Restaurant::factory()->has($categories)->has($staffMember, 'staff'); User::factory()->vendor()->has($restaurant)->create(); }}
And add a helper method to TestCase
to retrieve the seeded vendor user.
tests/TestCase.php
namespace Tests; use App\Enums\RoleName; use App\Models\User; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase{ use CreatesApplication; protected function getVendorUser(): User { return User::whereHas('roles', function ($query) { $query->where('name', RoleName::VENDOR); })->first(); }}
Finally, create the WithTestingSeeder
trait. It will seed the database using TestingSeeder
and register permissions using AuthServiceProvider
.
When the WithTestingSeeder
trait is included in a test case, the setUpWithTestingSeeder
method is invoked automatically. We do not need to call it manually anywhere.
tests/Traits/WithTestingSeeder.php
namespace Tests\Traits; use App\Providers\AuthServiceProvider; trait WithTestingSeeder{ public function setUpWithTestingSeeder() { $this->artisan('db:seed', ['--class' => 'TestingSeeder']); (new AuthServiceProvider(app()))->boot(); }}
Create a new Api/RestaurantTest
file.
php artisan make:test Api/RestaurantTest
We will have the following tests:
tests/Feature/Api/RestaurantTest.php
namespace Tests\Feature\Api; use App\Models\City;use App\Models\User;use Illuminate\Foundation\Testing\RefreshDatabase;use Tests\TestCase;use Tests\Traits\WithTestingSeeder; class RestaurantTest extends TestCase{ use RefreshDatabase; use WithTestingSeeder; public function test_admin_can_view_restaurants(): void { $admin = User::factory()->admin()->create(); $response = $this ->actingAs($admin) ->getJson(route('api.admin.restaurants.index')); $response->assertOk(); } public function test_admin_can_view_restaurant(): void { $admin = User::factory()->admin()->create(); $vendor = $this->getVendorUser(); $response = $this ->actingAs($admin) ->getJson(route('api.admin.restaurants.show', $vendor->restaurant)); $response->assertOk(); } public function test_admin_can_update_restaurant() { $admin = User::factory()->admin()->create(); $vendor = $this->getVendorUser(); $response = $this ->actingAs($admin) ->putJson(route('api.admin.restaurants.update', $vendor->restaurant), [ 'restaurant_name' => 'Updated Restaurant Name', 'city_id' => City::where('name', 'Other')->first()->id, 'address' => 'Updated Address', ]); $response->assertAccepted(); }}
Let's look into traits first.
use RefreshDatabase;use WithTestingSeeder;
The RefreshDatabase
trait will automatically run the migrate:fresh
command for each test.
Laravel will always call the setUpWithTestingSeeder
method in the WithTestingSeeder
trait after RefreshDatabase
. We do not need to worry about the order there.
Now in test methods, we can create an admin using UserFactory
using the admin
method we added earlier.
$admin = User::factory()->admin()->create();
Then we can make a request as an admin user by calling the actingAs
method.
Example:
$response = $this ->actingAs($admin) ->getJson(route('api.admin.restaurants.index'));
We make JSON requests to routes by calling the getJson
, putJson
, patchJson
, and deleteJson
methods. These methods are helpful when working with APIs. They automatically add Content-Type: application/json
and Accept: application/json
headers to tell the server that we expect JSON in response like the client would.
After making a request, the response is returned. Then we can assert what response status is using these methods:
assertOk()
expects the status code to be 200;assertCreated()
expects the status code to be 201;assertAccepted()
expects the status code to be 202;All helper methods asserting status codes can be seen in the Illuminate\Testing\Concerns\AssertsStatusCodes.php
file.
Optionally you can assert for a specific code using the assertStatus($status)
method.
Data is passed as a second argument when calling methods that submit data.
Example:
$url = route('api.admin.restaurants.update', $vendor->restaurant);$data = [ 'restaurant_name' => 'Updated Restaurant Name', 'city_id' => City::where('name', 'Other')->first()->id, 'address' => 'Updated Address',]; $response = $this ->actingAs($admin) ->putJson($url, $data);
Now you can run tests using the test
Artisan command.
php artisan test
PASS Tests\Unit\ExampleTest✓ that true is true PASS Tests\Feature\Api\RestaurantTest✓ admin can view restaurants 1.12s✓ admin can view restaurant 0.24s✓ admin can update restaurant 0.25s ... PASS Tests\Feature\ProfileTest✓ profile page is displayed 0.03s✓ profile information can be updated 0.02s✓ email verification status is unchanged when the email address is un… 0.02s✓ user can delete their account 0.06s✓ correct password must be provided to delete account 0.06s Tests: 27 passed (59 assertions)Duration: 2.98s
Optionally you can run only specific tests using the --filter
flag. It will try to match the class or function name.
Example running tests that include Restaurant
.
php artisan test --filter Restaurant
PASS Tests\Feature\Api\RestaurantTest✓ admin can view restaurants 1.09s✓ admin can view restaurant 0.26s✓ admin can update restaurant 0.25s Tests: 3 passed (3 assertions)Duration: 1.67s
Another example is running tests that update
anything.
php artisan test --filter update
PASS Tests\Feature\Api\RestaurantTest✓ admin can update restaurant 1.13s PASS Tests\Feature\Auth\PasswordUpdateTest✓ password can be updated 0.07s✓ correct password must be provided to update password 0.06s PASS Tests\Feature\ProfileTest✓ profile information can be updated 0.03s Tests: 4 passed (15 assertions)Duration: 1.38s
More information on testing can be found in the Official Laravel Documention.