Using TDD to Create a RESTful API with Laravel 7.x

Armin Abbasi
9 min readJun 7, 2020

--

When I was learning about writing tests I couldn’t find a cohesive tutorial that takes you through all steps of the project implementation; therefore, I decided to share some of the knowledge that I gained during my struggles learning about TDD with other developers (in form of a simple project). Hopefully it will get you a grasp of this methodology.

Here is the main topics of this article:

  • Creating Feature tests (with Database assertions)
  • Implementing authentication using Passport
  • Developing API according to our tests

The API that we’re planning develop is supposed to be used for managing posts in our project, we have two types of users with different levels of access, admin users with full access and regular users whom are limited only to read access.

Most of the TDD tutorials suggest you to use SQLite as your testing database since it can run in memory and makes it super fast to run tests compared to conventional databases (such as MySQL or PostgreSQL), but SQLite has it’s own drawbacks (in time of writing this article SQLite does not let me create a default null column in a table) therefore, I decided to use a PostgreSQL as my testing database which I’m using in my production as well, you might want to use MySQL the difference is not really significant in this case.

- Setting up the project

First we need to create another database connection for our testing purposes in database config file config/database.php (just copy and past the primary connection and change environment variables, don’t forget to add new environment variables to your .env file as well).

Laravel database.php config file

Then we need to config phpunit.xml file which is located in root of our laravel project. We add the testing database connection that we created before to DB_CONNECTION variable:

<server name=”DB_CONNECTION” value=”pgsql_test”/>

Next we should add passport package to our project by running

$ composer require laravel/passport

Finally install passport via this command:

$ php artisan passport:install

Okay, we’re finished with installations for now.

I suggest you to create an alias for running phpunit by adding the line below to your current terminal sessions or your home directory .bashrc file :

alias phpunit=vendor/bin/phpunit

- Write tests first

Now we should write the whole logic of our API in descriptive naming methods, and following rules of TDD we should run the tests once to get an error from phpunit . Despite the fact that we know we haven’t developed any codes yet.

For a better organization I create a directory named Posts in tests/Feature and then inside this directory we create a class named PostManagementTest:

Content of this file:

I’ve written 8 tests methods that are checking our API’s behaviour in both positive and negative ways.

Our test class PostManagementTest extends Laravel’s TestCase in order to convert it to a proper test suite (In fact Laravel’s TestCase class overrides phpunit’s TestCase and adds more features that is used by Framework itself, for example it will make IOC container’s implementations available in your test classes).

In terms of interactions with database there are two traits that are used more often, these are DatabaseMigrations and RefreshDatabase , these traits will override the setUp() and tearDown() methods of the test case, and ease the process of working with database for you.

But there’s a key difference with these two, DatabaseMigrations will run a php artisan migrate:fresh each time before execution of a test (overrides setUp() method) so you have a fresh database in each test methods; but, with the cost of running a database migration every time running a test method (like 8 times for a test class in our case), which is not efficient at all!

The latter RefreshDatabase will run a migration only once per test classes, and keeps the database fresh in each tests by using database transactions, this trait will keep the data in database during execution of a test method and then rollbacks the transaction by the end of each tests (by overriding tearDown() method), now you know the reason behind selecting RefreshDatabase over the DatabaseMigrations.

In order to provide necessary database records for each test I added the below lines to the setUp() method, what it does is seeding database with classes that it has taken as a parameter (an array of seeder class names), seeding will be done respectively; therefore, we have roles table populated before the users table:

$this->seed([
'RolesTableSeeder',
'UsersTableSeeder',
]);

Describing test methods:

api_is_accessible this is the simple one, it sends a GET request to our api endpoint and makes sure it’s working properly with a 200 http status code.

admin_can_create_post in this method with the help of Passport’s mocking method actingAs() , we’re making a request as our Admin user which has enough privilege to create a post, we’re sending the data using a POST request to our endpoint and first check the success of our request by chaining assertStatus(201) to the end of our json method, and second checking the database content by $this->assertDatabaseHas('posts', $data);

admin_can_update_a_post here like the method above we’re mocking admin user, and sending a PUT request to update a post we created using Laravel factory feature, the same routine as before we check http status code along with a database assertion.

invalid_input_is_not_acceptable checking if the input validation works properly, in case of invalid input we should receive a 422 http status code.

user_not_allowed_to_create_post this time we login as a regular user and send some data with POST request to check if our authorization works, also we check database for not having the user’s provided data.

user_not_allowed_to_update_posts making sure user cannot update any posts.

user_not_allowed_to_delete_a_post checking if user can delete a post.

Let’s run the tests once to face the big disappointment:

$ phpunit tests/

phpunit error message
Yup! it’s red

We’re done with the tests and it’s time to implement the idea.

- Create migrations, models and seeds

Create roles table by running

$ php artisan make:migration create_roles_table

<?php

use
Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateRolesTable extends Migration
{
/**
* Run the migrations.
*
*
@return void
*/
public function
up()
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
*
@return void
*/
public function
down()
{
Schema::dropIfExists('roles');
}
}

Next, we add role_id foreign key to users table:

$ php artisan make:migration add_role_id_to_users_table

<?php

use
Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddRoleIdToUsersTable extends Migration
{
/**
* Run the migrations.
*
*
@return void
*/
public function
up()
{
Schema::table('users', function (Blueprint $table) {
$table->unsignedBigInteger('role_id')->nullable();
$table->foreign('role_id')
->references('id')
->on('roles');
});
}

/**
* Reverse the migrations.
*
*
@return void
*/
public function
down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('role_id');
});
}
}

And finally create migration for posts table:

$ php artisan make:migration create_posts_table

<?php

use
Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
/**
* Run the migrations.
*
*
@return void
*/
public function
up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
*
@return void
*/
public function
down()
{
Schema::dropIfExists('posts');
}
}

In the end we need to run php artisan migrate to migrate all these changes.

With running this command for the first time we also create tables that are needed by the Passport package, which we installed earlier.

Let’s create the models:

User model doesn’t have any special magic in it, we added HasApiTokens to make the user model Passport compliant, protected $with = ‘role'; will eager load the role() relation every time we access an instance of this model, isAdmin() method checks if the given User instance is an admin, since we use this query frequently it will reduce amount of our code and looks more clean.

Now we need to populate our database with seeds

$ php artisan make:seed RolesTableSeeder

<?php

use
Illuminate\Database\Seeder;

class RolesTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
*
@return void
*/
public function
run()
{
$roles = [
[
'name' => 'user',
],
[
'name' => 'admin',
],
];

\App\Role::query()->insert($roles);
}
}

And users seeder:

$ php artisan make:seed UsersTableSeeder

<?php

use
App\Role;
use App\User;
use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
*
@return void
*/
public function
run()
{
$userRoleId = Role::query()
->where('name', 'user')
->first()
->id;

$adminRoleId = Role::query()
->where('name', 'admin')
->first()
->id;

$users = [
[
'email' => 'user@company',
'name' => 'Regular User',
'role_id' => $userRoleId,
'password' => bcrypt('12345'),
],
[
'email' => 'admin@company',
'name' => 'Admin User',
'role_id' => $adminRoleId,
'password' => bcrypt('12345'),
],
];

User::query()->insert($users);
}
}

We also need a factory for our posts table:

$ php artisan make:factory PostFactory --model=Post

<?php

/**
@var \Illuminate\Database\Eloquent\Factory $factory */

use
App\Post;
use Faker\Generator as Faker;

$factory->define(Post::class, function (Faker $faker) {
return [
'title' => $faker->sentence,
'content' => $faker->realText(),
];
});

Now we get to the final part, just keep up with me a bit more, it’ll be finished soon!

- Write the codes

We will implement authentication part now, for this purpose we need to tweak some files and create a controller, first we open the config/auth.php and change api’s guard driver to passport:

Laravel auth.php

Next, we call the Passport::routes method within the boot method of our app/Providers/AuthServiceProvider.php file.

Alright we’re done with tweaks and now we need to create a controller to handle authentication:

$ php artisan make:controller Api/AccessTokenController

There are two important methods in this controller that control the flow of authentication.

login() is used to post user input credentials (email and password) along with our app’s client secret and id to the passport’s built-in route (oauth/token ).

refresh() method is similar to login method but it uses refresh_token grant type and send’s user’s refresh token instead of email and password.

Next, we will implement our PostController:

$ php artisan make:controller Api/PostController

Now we have to create our custom request validation classes:

$ php artisan make:request CreatePostRequest

<?php

namespace
App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CreatePostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
*
@return bool
*/
public function
authorize(): bool
{
if (auth()->user()->isAdmin()) {
return true;
}

return false;
}

/**
* Get the validation rules that apply to the request.
*
*
@return array
*/
public function
rules()
{
return [
'title' => 'required|string',
'content' => 'required|string',
];
}
}

And for the update part:

php artisan make:request UpdatePostRequest

<?php

namespace
App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdatePostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
*
@return bool
*/
public function
authorize(): bool
{
if (auth()->user()->isAdmin()) {
return true;
}

return false;
}

/**
* Get the validation rules that apply to the request.
*
*
@return array
*/
public function
rules()
{
return [
'title' => 'string',
'content' => 'string',
];
}
}

And finally we update our route file (which is located inroutes/api.php)

Alright, now if we did everything right we should witness a nice green bar:

$ phpunit tests/

Mission accomplished!

I hope you enjoy this tutorial and learn a thing or two from it.

I tried to make this tutorial as efficient as possible, that's why you can see PostController is not benefiting from any specific design pattern but I might write another tutorial on refactoring this project using Repository design pattern and Unit testing.

--

--

Armin Abbasi

Engineering enthusiast. When not coding I enjoy a workout session, or a good read.