As I am learning more and more about Test driven development. I am getting fascinated with this approach of development and the enormous benefits that it brings for the application development.
So I decided to write a tutorial on how to approach TDD with a very simple CRUD application in Laravel.
For those of you unaware of these terms, here is what it means.
TDD Test-driven development (TDD) is a software development process that relies on the repetition of a very short development cycle: first the developer writes an (initially failing) automated test case that defines a desired improvement or new function, then produces the minimum amount of code to pass that.
CRUD The CRUD cycle describes the elemental functions of a persistent database. CRUD stands for Create, Read, Update and Delete. (Retrieve may occasionally be substituted for Read.) These functions are also descriptive of the data life cycle.
So, the TDD approach we are going to follow is very simple.
- Think of the feature we are going to implement
- Write Feature/unit test required to test the feature
- Run the test, keep writing code to take the test from green to red
- Once test passes, optimize and refactor the code
- Keep running the test in refactor process to verify you are not breaking the functionality
We are going implement a very basic To-Do application as an example for this tutorial. In this application, you can create, read, update and delete the Tasks.
Use coupon 5balloons on this Cloudways Affiliate URL to get special discount.
If you are following along, make sure you have following ready.
Alright, Let’s dig into the steps.
- Setup Model and Migrations
- Generate Model Factory
- Getting Started with Test Driven Development
- A user can read all the tasks [READ]
- A user can read a single task [READ]
- An authenticated user can create new task [CREATE]
- Authorized user can update the task [UPDATE]
- Authorized user can delete the task [DELETE]
- Run Full Test Suite
#1 Setup Model and Migrations
Let’s start thinking here of what our To-Do application will consist of, Since for this example we are considering a very basic To-Do, we can make it very simple with just two Model i.e. Tasks and Users.
And we can think of this relationship that exists between them.
A task is created by users.
A real life application will have many more models but we are going to go with these two for this example.
We already have a User
model available by default in our application at app directory. Let’s start by generating the authentication scaffolding.
php artisan make:auth
This will generate the basic layout file and also code for user authentication (login, logout, registration, forget password etc.)
Next, let’s generate model and controller for our Task model.
php artisan make:model Task -mr
This will generate a new Model file named Task.php
in our app directory, also we have appended the command with attributes -mr , this denotes that we also want database migration file to be generated and also a resourceful controller (Controller with default CRUD methods)
Let’s now go ahead and modify the database migration file for our Task database table.
Go to your code editor and navigate to folder database > migrations, you will find a migration file with name create_tasks_table
in this folder. This file was generated as part of make:model
artisan command.
It’s now a good time to think of what data we want to store in our tasks
table. For this simple example. Tasks will have a title and a description and also each and every task is associated with a user.
Modify the up()
method of the migration file like below.
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('user_id');
$table->string('title');
$table->text('description');
$table->timestamps();
});
}
Make sure you have your project connected to database. Run the following artisan command to migrate the database tables.
php artisan migrate
#2 Generate Model Factory
We are now ready to generate model factory for our database table. Model Factory will be used to seed our table with test data. We will also make use of Model Factory in our TDD tests.
Model Factory are stored at directory database > factories . By default, we already have a model factory for User model with name UserFactory.php
$factory->define(App\User::class, function (Faker $faker) {
return [
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'email_verified_at' => now(),
'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
'remember_token' => str_random(10),
];
});
Let’s now generate Model Factory for our Tasks table.
Run the following command on your terminal at project root directory.
php artisan make:factory TaskFactory --model=Task
We will make use of Faker library to generate dummy content for the title and description of tasks table.
$factory->define(App\Task::class, function (Faker $faker) {
return [
'title' => $faker->sentence,
'description' => $faker->paragraph,
'user_id' => factory('App\User')->create()->id,
];
});
We can now test our model factory. Run php artisan tinker
in your terminal / command-line.
We can seed data into our database using factory method.
factory('App\Task',20)->create();
This will create 20 rows in your tasks table, and since we have associated user_id to with a new user. This will also create 20 users in the process.
#3 Getting Started with Test Driven Development
Let’s get started with TDD, We will create our tests in folder named tests in the application root directory. This consist of two folders by default Feature
and Unit
. And there is already a test named ExampleTest.php
in both the folders. We can remove this both tests.
Before we start creating a new test.
We don’t want to work with our local database, since that might be already populated with other data. For the tests we should be able to whip up exactly what is needed for our test requirements.
Thus we will use sqlite memory database.
Let’s make some configuration changes to the phpunit.xml file, which is located at the root folder of your laravel application. and set the environment variable as given below
<php>
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="MAIL_DRIVER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
</php>
This tells out phpunit to use sqlite and not to use actual database, just do everything in memory.
Sweet!
#4 A user can read all the tasks [READ]
Let’s create our First Test. Go to your tests > Feature directory and create a new file with name TasksTest.php
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class TasksTest extends TestCase
{
use DatabaseMigrations;
}
This is the skeleton of our new Test Class. As you notice that we are importing DatabaseMigration trait in our test. This denotes that for every test we will migrate the database if required and once the test is completed we will roll it back (undo the migrations). Thus for every test we will have a fresh blank database to work with.
Let’s start by adding our first feature test in the Test Class. The first feature we can think of is that, user should be able to read all the tasks in the database. Add a new test that says exactly the same.
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class TasksTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function a_user_can_read_all_the_tasks()
{
//Given we have task in the database
$task = factory('App\Task')->create();
//When user visit the tasks page
$response = $this->get('/tasks');
//He should be able to read the task
$response->assertSee($task->title);
}
}
That is our first test, we have made use of our factory method to whip-up a new task in the database and we then go to tasks url and assert that we see the task title. Very Simple first test.
Running the test
Now to run this test, go to your application terminal and run the following command.
vendor/bin/phpunit --filter a_user_can_read_all_the_tasks
By using –filter we can run a single Test Class or a single Test method. Of-course the test will fail, since we don’t have any code against the test.
Now, Let’s write our code to make the test form red to green.
Before anything, we need a route file that points to the tasks url. Go to your route file web.php
which is located under routes folder and add a GET route for the tasks url.
Route::get('/tasks', 'TaskController@index');
Now we’ll modify the index
method of TaskController
to get list of tasks and pass it on to the view.
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$tasks = Task::latest()->get();
return view('tasks.index',compact('tasks'));
}
Here we make use of Task Eloquent model to get all the tasks sorted by created time and pass it on to the view.
Next, as you may have guessed, we need to create a new view file named index.blade.php
under folder resources > tasks .
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="page-header">
<h2>All Tasks</h2>
</div>
@foreach($tasks as $task)
<div class="card">
<div class="card-header">{{$task->title}}</div>
<div class="card-body">
{{$task->description}}
</div>
</div>
@endforeach
</div>
</div>
</div>
@endsection
Alright, we are now all set to run our test again.
There you go, we have now our test giving out green colors, and we have not yet looked at the browser to see if it works. And since now our test is passing, we are sure it’s working in browser too.
#5 A user can read a single task [READ]
Let’s move on to our next feature i.e. a user can read a single task. Which basically means that each and every task has a unique URI path and when we visit that path we can see the tasks details.
Let’s create a new test class a_user_can_read_a_single_task, I have added comments in the method
/** @test */
public function a_user_can_read_single_task()
{
//Given we have task in the database
$task = factory('App\Task')->create();
//When user visit the task's URI
$response = $this->get('/tasks/'.$task->id);
//He can see the task details
$response->assertSee($task->title)
->assertSee($task->description);
}
In this test, we have created a task in our database and we assert that we see the task detail when we visit the single task page. When we run the test this will obviously fail since we don’t have the necessary code in place yet to make it work.
Alright, let’s make the necessary efforts to make this work.
First off , we know that we don’t have necessary route in our web.php
file to make this request work. So, we will create a new GET route in the routes file.
Route::get('/tasks/{task}','TaskController@show');
Next, modify the show
method in your TaskController
to get the Task detail by route model binding and then pass on the task to the view.
/**
* Display the specified resource.
*
* @param \App\Task $task
* @return \Illuminate\Http\Response
*/
public function show(Task $task)
{
return view('tasks.show',compact('task'));
}
Now we know that we need to create a new blade file show.blade.php
in our resources > views > tasks folder.
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="page-header">
<h2>Task Detail</h2>
</div>
<div class="card">
<div class="card-header">{{$task->title}}</div>
<div class="card-body">
{{$task->description}}
</div>
</div>
</div>
</div>
</div>
@endsection
We now have all the required code in place, Run the test again.
And now we get green, Let’s move on to another feature.
#6 An authenticated user can create new task [CREATE]
Let’s move on to the Create part of the CRUD process. Next up , let’s create a test where we assert that an authenticated user can create new task in the database.
/** @test */
public function authenticated_users_can_create_a_new_task()
{
//Given we have an authenticated user
//And a task object
//When user submits post request to create task endpoint
//It gets stored in the database
}
This is the skeleton of our new test method, read the comments in the method to understand what we are trying to achieve. Let’s go ahead and write the test itself.
/** @test */
public function authenticated_users_can_create_a_new_task()
{
//Given we have an authenticated user
$this->actingAs(factory('App\User')->create());
//And a task object
$task = factory('App\Task')->make();
//When user submits post request to create task endpoint
$this->post('/tasks/create',$task->toArray());
//It gets stored in the database
$this->assertEquals(1,Task::all()->count());
}
Run the test, and it will give you red color. We don’t have the end-point to create a new task yet.
Let’s dive in to to make this test into green.
First, we need to create an endpoint to create a new task. Add a new route to the web.php route file.
Route::post('/tasks/create','TaskController@store');
Modify the store method in TaskController
to add logic to add new task into the database.
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$task = Task::create([
'title' => $request->get('title'),
'description' => $request->get('description'),
'user_id' => Auth::id()
]);
return redirect('/tasks/'.$task->id);
}
Run the test and it will still fail, because we haven’t yet gaurded our properties in the Task model. Add this line to your Task model.
class Task extends Model
{
protected $guarded = [];
}
Run the test again, and you should get green !
While we are at it, we need to make sure that unauthenticated users should not be able to hit the endpoint to create a new task. Let’s add another test to verify this.
/** @test */
public function unauthenticated_users_cannot_create_a_new_task()
{
//Given we have a task object
$task = factory('App\Task')->make();
//When unauthenticated user submits post request to create task endpoint
// He should be redirected to login page
$this->post('/tasks/create',$task->toArray())
->assertRedirect('/login');
}
Here we are asserting that when an unathenticated user tries to hit endpoint he should be redirected to the login page. Run the test and it will fail with this error
Caused by
PDOException: SQLSTATE[23000]: Integrity constraint violation: 19 tasks.user_id may not be NULL
This means that unathenticated can still hit the endpoint. We need to fix this.
Go to your TaskController.php file and modify the contruct method to apply auth
middleware.
public function __construct(){
$this->middleware('auth')->except(['index','show']);
}
We have applied auth middleware to all the methods of TaskController except index and show.
Run the test again, and you should get green.
Now that we have the endpoint API ready. That means we can now go ahead and create the front-end ‘form page’ to submit data to this API.
Add a new route to your web.php
routes file.
Route::get('/tasks/create','TaskController@create');
Go to your TaskController.php
and modify the create method to return the view file to create a new task.
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('tasks.create');
}
Next up we need to add the view file named create.blade.php
in folder resources > views > tasks
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header"><strong>Add a new task</strong></div>
<div class="card-body">
<form method="POST" action="/tasks/create">
{{csrf_field()}}
<div class="form-group">
<input type="text" class="form-control" name="title" id="title" placeholder="Enter a title for your task">
</div>
<div class="form-group">
<textarea class="form-control" name="description" placeholder="Provide a description for your task" rows="8">
</textarea>
</div>
<button class="btn btn-primary" type="Submit" >Add Task</button>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
Make sure your create task view files has following.
- Form with method POST and action pointing to /tasks/create
- Fields with name title and description and a submit button.
- {{csrf_field()}} to prevent cross site scripting attack.
We already have our endpoint ready and tested. When you submit details from your create task form it should just work.
Testing Validation.
We have our Create New Task form page and the endpoint ready. But we have not handled the validation yet. If you submit your form without entering any data it will post null data to the endpoint and we will get an error from the database regarding the NULL value.
Let’s fix that. We will add a new tests to assert the validation.
/** @test */
public function a_task_requires_a_title(){
$this->actingAs(factory('App\User')->create());
$task = factory('App\Task')->make(['title' => null]);
$this->post('/tasks/create',$task->toArray())
->assertSessionHasErrors('title');
}
/** @test */
public function a_task_requires_a_description(){
$this->actingAs(factory('App\User')->create());
$task = factory('App\Task')->make(['description' => null]);
$this->post('/tasks/create',$task->toArray())
->assertSessionHasErrors('description');
}
Here we have added two new test to verify that we are validating both the fields title and description. Also we specifically passed on a null value to the endpoint and then we are asserting that the session has error with the specified field name in it.
Run the test and it will fail. Since we don’t have validation in place yet.
Let’s add code to validate the form fields. Modify the store method of TaskController
public function store(Request $request)
{
$request->validate([
'title' => 'required',
'description' => 'required'
]);
$task = Task::create([
'title' => $request->get('title'),
'description' => $request->get('description'),
'user_id' => Auth::id()
]);
return redirect('/tasks/'.$task->id);
}
Run the test again and it should pass this time.
We also need to handle this on your front-end to show error messages to the user. Add this code below your form to show error messages
@if(count($errors))
<div class="alert alert-danger">
@foreach($errors->all() as $error)
<li>
{{$error}}
</li>
@endforeach
</div>
@endif
And now we have the validation working.
We are making good progress ! Let’s move on to the next part.
#7 Authorized user can update the task [UPDATE]
Let’s move on to the Update part of our CRUD application. An authorized user should be able to update his tasks.
Add a new test in TasksTest
test class.
/** @test */
public function authorized_user_can_update_the_task(){
//Given we have a signed in user
$this->actingAs(factory('App\User')->create());
//And a task which is created by the user
$task = factory('App\Task')->create(['user_id' => Auth::id()]);
$task->title = "Updated Title";
//When the user hit's the endpoint to update the task
$this->put('/tasks/'.$task->id, $task->toArray());
//The task should be updated in the database.
$this->assertDatabaseHas('tasks',['id'=> $task->id , 'title' => 'Updated Title']);
}
Here we have persisted a task in the database and then later we modify the title of the task and make a put request to /task/$task->id url. After that we assert that the title has been updated in the database.
Let’s run the test, and it will fail since we don’t have the API endpoint ready yet to update the task.
Let’s create the update task endpoint. Add the following entry into the web.php
routes file.
Route::put('/tasks/{task}','TaskController@update');
We now need to modify the update method of the TaskController class.
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Task $task
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Task $task)
{
$task->update($request->all());
return redirect('/tasks/'.$task->id);
}
Run the test again and it should give you green
Next, we need to make sure that unauthorized user’s should not be able to edit/update the task details. Think about that in a way that you created task but now any authenticated user can edit it. We need to prevent this
Let’s add a new test for this named unauthorized_user_cannot_update_the_task
/** @test */
public function unauthorized_user_cannot_update_the_task(){
//Given we have a signed in user
$this->actingAs(factory('App\User')->create());
//And a task which is not created by the user
$task = factory('App\Task')->create();
$task->title = "Updated Title";
//When the user hit's the endpoint to update the task
$response = $this->put('/tasks/'.$task->id, $task->toArray());
//We should expect a 403 error
$response->assertStatus(403);
}
Here we sign-in as a user and try to hit the update endpoint for a task which is not created by the user. In such case we expect to get UnAuthorized Error or a 403 response status.
Run the test and it will fail, since we don’t have code yet to prevent this.
Let’s fix this
We need to create a Policy which will check if the user is authorized to perform certain actions, Go to your terminal / command-prompt and run the below artisan command
php artisan make:policy TaskPolicy --model=Task
This artisan command will create a new File named TaskPolicy in folder app > Policies
Open TaskPolicy.php
and modify the update method
/**
* Determine whether the user can update the task.
*
* @param \App\User $user
* @param \App\Task $task
* @return mixed
*/
public function update(User $user, Task $task)
{
return $task->user_id == $user->id;
}
We have denoted that for update action the tasks user_id should be same as user’s id. Which makes sense, user should only be able to perform edit action on the task that which he owns.
Next, we need to register this policy in AuthServiceProvider.php
which is located in directory app > Providers
Update the policies array variable as below
protected $policies = [
'App\Task' => 'App\Policies\TaskPolicy',
];
Next modify the update
method to include authorize code in place
public function update(Request $request, Task $task)
{
$this->authorize('update', $task);
$task->update($request->all());
return redirect('/tasks/'.$task->id);
}
Run the test again and this time it should pass
Let’s move to the front end part of update task. Add a new route to the web.php
routes file
Route::get('/tasks/{task}/edit','TaskController@edit');
Modify the edit method of TaskController
/**
* Show the form for editing the specified resource.
*
* @param \App\Task $task
* @return \Illuminate\Http\Response
*/
public function edit(Task $task)
{
return view('tasks.edit',compact('task'));
}
Add a new view file to show the edit task form named edit.blade.php in resources > views > tasks folder.
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header"><strong>Update task</strong></div>
<div class="card-body">
<form method="POST" action="/tasks/{{$task->id}}">
{{csrf_field()}}
{{method_field('PUT')}}
<div class="form-group">
<input type="text" class="form-control" name="title" id="title" placeholder="Enter a title for your task" value="{{$task->title}}">
</div>
<div class="form-group">
<textarea class="form-control" name="description" placeholder="Provide a description for your task" rows="8">{{$task->description}}
</textarea>
</div>
<button class="btn btn-primary" type="Submit" >Edit Task</button>
</form>
</div>
</div>
@if(count($errors))
<div class="alert alert-danger">
@foreach($errors->all() as $error)
<li>
{{$error}}
</li>
@endforeach
</div>
@endif
</div>
</div>
</div>
@endsection
We have used {{method_field('PUT'}}
directive in our form to denote that we want to make a PUT request to the action URL.
Next let’s add an edit button to show.blade.php page.
We have added a bootstrap card-footer for the edit button
<div class="card-footer">
@can('update', $task)
<a href="/tasks/{{$task->id}}/edit" class="btn btn-warning">Edit Task</a>
@endcan
</div>
We are making use of @can blade directive. Which means that the Edit button will only be shown if the user has update authorization.
We are alomst there, Let’s move on to the next and last part of CRUD.
#8 Authorized user can delete the task [DELETE]
Authorized user should also be able to delete their tasks, Let’s add a new test for this named authorized_user_can_delete_the_task
/** @test */
public function authorized_user_can_delete_the_task(){
//Given we have a signed in user
$this->actingAs(factory('App\User')->create());
//And a task which is created by the user
$task = factory('App\Task')->create(['user_id' => Auth::id()]);
//When the user hit's the endpoint to delete the task
$this->delete('/tasks/'.$task->id);
//The task should be deleted from the database.
$this->assertDatabaseMissing('tasks',['id'=> $task->id]);
}
Here, we try to make a delete request to the url and then we assert that the database table tasks no longer have the task.
Run the test and it will give you red.
Let’s make necessary code changes to take the test from red to green.
Add a new entry to route file web.php
Route::delete('/tasks/{task}','TaskController@destroy');
Next, modify the destroy method of TaskController to handle the delete request.
/**
* Remove the specified resource from storage.
*
* @param \App\Task $task
* @return \Illuminate\Http\Response
*/
public function destroy(Task $task)
{
$this->authorize('update', $task);
$task->delete();
return redirect("/tasks");
}
Run the test again and it should now pass.
While we are at it, let’s add another test that asserts that unauthorized user should not be able to delete tasks.
/** @test */
public function unauthorized_user_cannot_delete_the_task(){
//Given we have a signed in user
$this->actingAs(factory('App\User')->create());
//And a task which is not created by the user
$task = factory('App\Task')->create();
//When the user hit's the endpoint to delete the task
$response = $this->delete('/tasks/'.$task->id);
//We should expect a 403 error
$response->assertStatus(403);
}
This test should pass, since we have already added the authorize method in the destroy method.
Now since we have our tests passing, let’s move to the front-end part of this. Add the following snippet to show.blade.php
<form style="float:right" method="POST" action="/tasks/{{$task->id}}">
{{csrf_field()}}
{{method_field('DELETE')}}
<button class="btn btn-danger" type="submit">Delete</button>
</form>
This has the form with button to delete the task, put this snippet inside @can directive so that it is only visible to user who are authorized to delete the task.
Run Full Test Suite
We are done with the CRUD implementation in Laravel with TDD. It’s good idea to run the full test suite to check that everything is running as expected and we did not broke anything in our progress.
Run the full suite
vendor/bin/phpunit
Everything should pass
Great ! We wrote 10 tests and made 14 assertions.
Hope you had fun learning the basics of TDD with Laravel and you also realized the benefits it brings to your development process.