Test-Driven Development (TDD) in Laravel 8

Macdonald Chika
7 min readMay 31, 2021

--

Before we delve into Test-Driven development, let's first define the term TDD.

What is Test-Driven Development (TDD)?

According to Wikipedia, “Test-Driven Development is a software development process relying on software requirements being converted to test cases before the software is fully developed, and tracking all software development by repeatedly testing the software against all test cases”.

It is a software development approach that emphasizes writing bug-free codes by developing test cases to specify and validate what the code will do.

Here is what Test-Driven Development(TDD) cycle looks like:

  1. Write a test
  2. Run the test — which will fail
  3. Write some code
  4. Run the test again
  5. Make changes to code to make the test pass(refactor)
  6. Repeat

How To Write Tests in Laravel

For the purpose of this article, we will create a simple CRUD API to create, read, update and delete blog posts. let's begin by creating a fresh Laravel project using Composer.

Step 1: Create and initialize the project

composer create-project laravel/laravel blogcd blog

You can run the command below to be sure that everything works fine.

php artisan serve 

Step 2: Set up your test suite

Update your phpunit.xml file in your root directory. Uncomment these lines of code

<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>

The updated file should look like this:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
</include>
</coverage>
<php>
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_DRIVER" value="array"/>
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
<server name="MAIL_MAILER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
<server name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit>

We uncommented or added those lines of code to ensure that PHPUnit uses :memory: database so that tests run faster. Now that our test suite is set up, let’s set up our base test file TestCase.php

<?php

namespace
Tests;

use Faker\Factory as Faker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Artisan;

abstract class TestCase extends BaseTestCase
{
use CreatesApplication, RefreshDatabase;

protected $faker;

/**
* Sets up the tests
*/
public function
setUp(): void
{
parent::setUp();

$this->faker = Faker::create();

Artisan::call('migrate'); // runs the migration
}


/**
* Rolls back migrations
*/
public function
tearDown(): void
{
Artisan::call('migrate:rollback');

parent::tearDown();
}
}

You would notice that we use the RefreshDatabase trait because it is often useful to reset our database after each test so that data from a previous test does not interfere with subsequent tests. You may also notice that we added two methods setUp() and tearDown(). The former is run by the test runner prior to each test and the latter after each test. They help keep your test code clean and flexible.

Step 3: Create and write your test

To create a test file in Laravel 8, simple run this artisan command

php artisan make:test PostTest

The command above creates a PostTest.php file in the tests/Feature directory.

The next thing we need to do is write our actual test.

Note: You will need to either prefix your methods name with test i.e testCanCreateAPost or use the /** @test */ annotation. That way PhpUnit knows that it needs to run your test.

<?php

namespace
Tests\Feature;

use Tests\TestCase;

class PostTest extends TestCase
{
/** @test*/
public function
canCreateAPost()
{
$data = [
'title' => $this->faker->sentence,
'description' => $this->faker->paragraph
];

$response = $this->json('POST', '/api/v1/posts', $data);

$response->assertStatus(201)
->assertJson(compact('data'));

$this->assertDatabaseHas('posts', [
'title' => $data['title'],
'description' => $data['description']
]);
}
}

Step 4: Run your test

Now we need to run our test with PHPUnit as thus:

./vendor/bin/phpunit

or Laravel artisan command which is pretty cool as it provides verbose test reports in order to ease development and debugging:

php artisan test
TDD in Laravel 8

Oops! Our test FAILED! is this good? Yes, it is in fact fine for our test to fail as it follows the second rule of TDD that it should Fail after creating the test.

So why did our test fail?

The error says the expected response code is 201 — as we have asserted in our test — but received 404 (Not found). This means that the endpoint [POST]‘/api/v1/posts’ does not exist and we need to create one. This brings us to the next step.

Step 5: Make Test Pass

What we will be doing in this step is to make our test pass. First to get rid of the 404 error, let's create an endpoint.

CREATE THE ENDPOINT IN YOUR ROUTE FILE

let’s go to our route file located at ‘routes/api.php’ and create that endpoint. All routes created in this file are automatically prefixed with ‘/api’.

php artisan make:controller --resource

You can either run this command to create a RESTful controller in the /app/Http/Controllers/Api directory or you can create it manually.

Now let’s debug our controller and validate our request

In the store method, where the POST request goes, return a response as thus:

<?php

namespace
App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class PostController extends Controller
{

public function index()
{
//
}

public function create()
{
//
}

public function store(Request $request)
{
return response()
->json([
'message' => 'Post created'
]);
}


public function show($id)
{
//
}


public function edit($id)
{
//
}


public function update(Request $request, $id)
{
//
}

public function destroy($id)
{
//
}
}

We will create a request file containing some validation rules to validate the data going into our database.

php artisan make:request CreatePostRequest

Remember to change false to true in the authorize() method

<?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()
{
return true;
}

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

We are going to import and pass our CreatePostRequest to the store method of our controller. The store() method in our controller should now look like this:

public function store(CreatePostRequest $request)
{
return response()
->json([
'message' => 'Post created'
]);
}

Note: Remember to import the CreatePostRequest class.

CREATE YOUR MODEL AND MIGRATION

php artisan make:model Post -m

This artisan command creates our model and migration. The -m flag creates a migration file under database/migrations directory. We will add title and description columns in our migration as thus:

<?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->string('description');
$table->timestamps();
});
}

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

And in our Model, we have to define the hidden and fillable fields.

<?php

namespace
App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;


class Post extends Model
{
use HasFactory;

protected $fillable = [
'title',
'description'
];
}

Once that is done, we will edit our store method as thus:

public function store(CreatePostRequest $request)
{
$data = $request->validated(); // only validated request

$post = Post::create($data); // creates and return post

return
response()
->json([
'message' => 'Post created'
]);
}

Note: remember to import the Post class in your controller

RE-RUN TESTS

php artisan test

Our test Failed again but this time it says that it can’t assert that 201 is identical to 200. This means that our endpoint returns a 200 response code instead of a 201. So let’s add a 201 response code — which means that the resource was successfully created

public function store(CreatePostRequest $request)
{
$data = $request->validated(); // only validated request

$post = Post::create($data); // creates and return post

return
response()
->json([
'message' => 'Post created'
], 201);
}

let's re-run our test again.

Note: if you get a 500 response code, you can take a look at the log file at /storage/logs/laravel.log.

TDD in Laravel 8

Now we get another error. It can’t assert that a certain JSON exists, which was defined in our test here

$data = [
'title' => $this->faker->sentence,
'description' => $this->faker->paragraph
];
$response = $this->json('POST', '/api/v1/posts', $data);$response->assertStatus(201)
->dump() // use this to know what data is being returned
->assertJson(compact('data')); // this line here

To solve this, we need to return the created resource to assert that it matches the data that is being sent to the endpoint. So, within our store() method will add a ‘data’ object that returns the newly created resource to our payload.

public function store(CreatePostRequest $request)
{
$data = $request->validated(); // only validated request

$post = Post::create($data); // creates and return post

return
response()
->json([
'data' => $post,
'message' => 'Post created'
], 201);
}

Once that is done, we will re-run our test.

TDD in laravel8

WELL DONE! You have made the test pass.

--

--