How to Unit Test a Laravel API with the Pest Framework

February 18, 2021
Written by
Dotun Jolaoso
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Diane Phan
Twilion

How to Unit Test a Laravel API with the Pest Framework

Pest is a new testing PHP Framework developed by Nuno Maduro. While Pest itself is built on top of PHPUnit, the popular and widely adopted PHP testing framework, Pest aims to provide a better experience for writing tests. The philosophy is simple. Keep the TDD experience simple and elegant by providing expressive interfaces.

In this tutorial, we’ll be looking at how to get started using Pest in a Laravel project. Using the test-driven approach, we’ll be building a simple to-do application that allows you to create, edit, update and delete tasks.

Technical Requirements

  • PHP version 7.3 or higher. Pest requires PHP 7.3+ to work.
  • Laravel 8.
  • Composer.
  • A basic understanding of PHPUnit.
  • A basic understanding of SQLite. We’ll be making use of SQLite because it makes running our tests faster.

Set up Laravel

There are different ways to set up a new Laravel project. You can do so via the Laravel installer or by using Composer. For the sake of this tutorial, we’ll be using Composer.

Run the following command in your terminal:

$ composer create-project --prefer-dist laravel/laravel pest-todo

This will set up a new Laravel project for us in the pest-todo directory.

Install Pest

Now that we’ve set up a new Laravel project, there are a couple of additional steps we need to carry out to set up Pest with Laravel. Type cd pest-todo to change into the new pest-todo directory, and then run the following command:


$ composer require pestphp/pest --dev

Next, we’ll be installing the Pest Plugin for Laravel. To do that, run the following command:

$ composer require --dev pestphp/pest-plugin-laravel

Once the plugin has been installed, run the following command:

$ php artisan pest:install

This will create a Pest.php file in the tests directory. The Pest.php file is autoloaded automatically and serves as an ideal place to recursively bind helper classes and traits to your tests. Laravel comes bundled with some example test files based on PHPUnit. Let’s change those tests to make use of Pest instead. Head over to the tests/Feature directory and take a look at the ExampleTest.php file. Here’s what it currently looks like:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBasicTest()
    {
        $response = $this->get('/');
        $response->assertStatus(200);
    }
}

To migrate this test to the corresponding Pest implementation, replace the content of the file with the following code:

<?php

it('has welcome page')->get('/')->assertStatus(200);

Much cleaner right? We’ve reduced this ExampleTest file from about 20 lines of code to just 2 lines while testing the exact same thing and producing the same result. That is, it visits the root URL at ‘/’ and asserts that an HTTP status code of 200 is returned.

Pest provides two functions for writing tests - test() and it(). Both functions accept a test description as the first argument and a closure that contains the test expectations as the second argument. They share the same syntax and behavior and you’re free to use either one you find fitting. I personally prefer using it() since they make your test cases read like a complete sentence.

Similarly, let’s make the ExampleTest.php file located in the tests/Unit directory use Pest as well. Replace the contents of the file with the following code:

<?php

test('basic')->assertTrue(true);

Next, use the following command to run the test suite:

$ ./vendor/bin/pest

All tests should be passing as seen in the image below.

Run the initial Pest framework tests in Laravel

Create the to-do Model, Migration and Controller

Our application is going to have a single Model called Todo. Laravel provides a handy command for generating a Model, Migration, and Controller for an entity all at once. To do that run the following command:

$ php artisan make:model Todo -m -c

A new migration file is created in the database/migrations directory at [TODAYSDATE]_create_todos_table.php. Next, add the following code to the up() method within the migration file:

 $table->string('name');
 $table->boolean('completed')->default(false);

Each to-do task will have a name attribute as well as a boolean completed attribute, with a default value of false. Next, edit the App/Models/Todo.php file with the following code:

<?php

namespace App\Models;

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

class Todo extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'completed'];
}

Here, we assign the name and completed attribute of the model to be mass-assignable.

Create the to-do factory

Laravel model factories provide a convenient way of seeding the database with data. This is very useful when it comes to testing. Run the following command to create a factory class for the to-do model:

$ php artisan make:factory TodoFactory

This will create TodoFactory.php for us in the database/factories directory. Edit the definition() method within the file to return an array similar to the one below:

return [
    'name' => 'Deploy Twilio Verify to Live',
    'completed' => false
];

The definition method returns the default set of attribute values that should be applied when creating a model using the factory.

Configure the database

We’ll be making use of an in-memory SQLite database for testing. This will make running our tests faster. Laravel already provides support for using a SQLite database for testing. Head over to the phpunit.xml file located at the root of your project’s directory and uncomment the following lines of code:

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

Write the tests

Now that we’ve done all the necessary setup and configuration, we can get started with writing the tests. These tests are required to have a functioning application and will provide the corresponding implementation to make sure all the tests pass.

Run the following Pest command to create a unit test file:

$ php artisan pest:test TodoTest --unit

This will create TodoTest.php in the tests/Unit directory. Replace the file’s code with the following code:

<?php

use App\Models\Todo;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(Tests\TestCase::class, RefreshDatabase::class);

it('does not create a to-do without a name field', function () {
    $response = $this->postJson('/api/todos', []);
    $response->assertStatus(422);
});

it('can create a to-do', function () {
    $attributes = Todo::factory()->raw();
    $response = $this->postJson('/api/todos', $attributes);
    $response->assertStatus(201)->assertJson(['message' => 'Todo has been created']);
    $this->assertDatabaseHas('todos', $attributes);
});

it('can fetch a to-do', function () {
    $todo = Todo::factory()->create();

    $response = $this->getJson("/api/todos/{$todo->id}");

    $data = [
        'message' => 'Retrieved To-do',
        'todo' => [
            'id' => $todo->id,
            'name' => $todo->name,
            'completed' => $todo->completed,
        ]
    ];

    $response->assertStatus(200)->assertJson($data);
});

it('can update a to-do', function () {
    $todo = Todo::factory()->create();
    $updatedTodo = ['name' => 'Updated To-do'];
    $response = $this->putJson("/api/todos/{$todo->id}", $updatedTodo);
    $response->assertStatus(200)->assertJson(['message' => 'To-do has been updated']);
    $this->assertDatabaseHas('todos', $updatedTodo);
});

it('can delete a to-do', function () {
    $todo = Todo::factory()->create();
    $response = $this->deleteJson("/api/todos/{$todo->id}");
    $response->assertStatus(200)->assertJson(['message' => 'To-do has been deleted']);
    $this->assertCount(0, Todo::all());
});

At the top of the file, the uses() method binds the TestCase class and the RefreshDatabase trait to the current test file. The base TestCase class is provided by Laravel and provides helper methods for working with the framework while testing. The RefreshDatabase trait takes care of migrating and resetting the database after each test so that data from a previous test does not interfere with subsequent tests.

Now, let’s go over what each test is doing:

  • it(‘does not create a to-do without a name field’) - Laravel provides several helpers for testing JSON APIs and their responses. Here we make use of the postJson helper to make a POST request to the api/todos endpoint passing in an empty array. Next, the assertStatus() method on the returned response ensures that an HTTP status code of 422 should be returned. This test ensures that a name field will always be present on the request payload.
  • it(‘can create a to-do’) - This test ensures that a to-do is created on making a POST request to the api/todos endpoint. We assert that an HTTP status code of 201 is returned and that the database actually contains the to-do using the assertDatabase() method.
  • it(‘can fetch a to-do’) - This test checks to see that a particular to-do task can be fetched using the ID. Using the create() method on the Todo Factory, a to-do task is created and stored in the database. Similarly, we assert that the status code returned is 200. The assertJson() method converts the response to an array and verifies that the given array exists within the JSON response that will be returned by the application.
  • it(‘can update a to-do’) - This test ensures that a to-do task can be updated and that the updated task can be found in the database.
  • it(‘can delete a to-do’) - This test ensures that a to-do task can be deleted and verifies that the total number of tasks contained within the database is zero.

To run the test suite, run the following command:

$ ./vendor/bin/pest --filter TodoTest

The test suite should be failing since we have not implemented any of the features.

Initial errors from running the test suite

Build the TO-DO application

Let’s provide the corresponding implementation of the tests we’ve written so far. Head over to the TodoController.php file in the app/Http directory and replace the file’s code with the following code:

<?php

namespace App\Http\Controllers;

use App\Models\Todo;
use Illuminate\Http\Request;

class TodoController extends Controller
{
    public function create(Request $request)
    {
        $request->validate($this->rules());

        $todo = Todo::create($request->only(['name']));

        $data = [
            'message' => 'To-do has been created',
            'todo' => $this->mapTodoResponse($todo)
        ];

        return response()->json($data, 201);
    }

    public function show(Todo $todo)
    {
        $data = [
            'message' => 'Retrieved To-do',
            'todo' => $this->mapTodoResponse($todo)
        ];

        return response()->json($data);
    }

    public function update(Todo $todo, Request $request)
    {
        $request->validate($this->rules());

        $todo->update($request->only(['name']));
        $todo->refresh();

        $data = [
            'message' => 'To-do has been updated',
            'todo' => $this->mapTodoResponse($todo)
        ];

        return response()->json($data);
    }

    public function delete(Todo $todo)
    {
        $todo->delete();

        $data = [
            'message' => 'To-do has been deleted'
        ];

        return response()->json($data);
    }

    protected function rules()
    {
        return [
            'name' => 'required|string|min:4'
        ];
    }

    protected function mapTodoResponse($todo)
    {
        return [
            'id' => $todo->id,
            'name' => $todo->name,
            'completed' => $todo->completed
        ];
    }
}
  • The create() method creates a new to-do task.
  • The show() method returns a given task based on it’s ID.
  • The update() method updates a to-do task.
  • The delete() method deletes a given to-do task.

Next, add the following routes to the routes/api.php file:

<?php

Route::get('/todos/{todo}', 'App\Http\Controllers\TodoController@show');
Route::post('/todos', 'App\Http\Controllers\TodoController@create');
Route::put('/todos/{todo}', 'App\Http\Controllers\TodoController@update');
Route::delete('/todos/{todo}', 'App\Http\Controllers\TodoController@delete');

Now that we’ve provided all the corresponding implementations for the tests, we can go back to running our tests and they should all be passing now. Run the test suite again with the following command:

$ ./vendor/bin/pest --filter TodoTest

Full set of unit tests passing

Conclusion

In this tutorial, we’ve seen how to go about writing unit tests for a Laravel application using the Pest testing framework. This tutorial can serve as a great guide for getting started with Pest as well as unit testing a Laravel application. The GitHub repository with the complete code for this project can be found here.

Dotun Jolaoso

Website: https://dotunj.dev/
Github: https://github.com/Dotunj
Twitter: https://twitter.com/Dotunj_