Test-Driven Development (TDD) in Laravel 8
Tags: php, testing, programming, laravel
Published: 2022-01-25 18:17:50


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 by using **Composer**.

**Step 1: Create and initialize the project**
`composer create-project laravel/laravel blog`

`cd 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 codes. 

`<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 **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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/95scp6coqhqjfuo6uzcu.png)

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

```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``



![Failed asserting 201 is identical to 200 - Test-Driven development in Laravel 8](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3s10mb2423bwnkjjkcy9.png)

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**.


![Unable to find Json - Test-Driven development in laravel 8](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w3ikl9r0ztdjsux0o5iv.png)

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.


![Test passed - Test-driven development in laravel 8](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bgwv461uyw2003oipegj.png)

WELL DONE! You have made the test pass.