Back to Course |
Laravel Web to Mobile API: Reuse Old Code with Services

Testing Admin Restaurant API Routes

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.


Setup Testing

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 Admin Restaurant API Test

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);

Running Tests

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.