My FeedDiscussionsHeadless CMS
New
Sign in
Log inSign up
Learn more about Hashnode Headless CMSHashnode Headless CMS
Collaborate seamlessly with Hashnode Headless CMS for Enterprise.
Upgrade ✨Learn more
Testing Laravel API With Pest Framework

Testing Laravel API With Pest Framework

Simon Moses Onazi's photo
Simon Moses Onazi
·Jan 8, 2022·

8 min read

Introduction

In a bit, to reduce the number of bugs shipped to a production environment to the barest minimum, automated testing is becoming an essential skill for developers. It is now, more than ever, crucial for developers to write rich test coverage, and trust me, your future self will thank you for adopting and writing tests on your code.

In this article, we will create a superficial post-application, which we will use to learn how to test laravel 8 using pest. You will no longer feel bored or confused when writing tests again.

Pest is an elegant PHP Testing Framework with a focus on simplicity. It was carefully crafted to bring the joy of testing to PHP.

After this, you should no longer be afraid of writing test cases, refactoring your code, and building a new feature.

Prerequisites

  • 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 MySQL.

Set up Laravel

There are multiple ways of installing a new Laravel project. In this article, we’ll use the Composer way.

Run the command below on your terminal:

$ composer create-project laravel/laravel pest-plugin-laravel

This will create a new Laravel project for us in the pest-plugin-laravel directory.

Install Pest

Let's install Pestphp packag and pest-plugin on our new Laravel project. Navigate to our newly created project cd pest-plugin-laravel to change into the pest-plugin-laravel directory, then run the below command:

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

The command below will create a Pest.php file in the tests directory. The Pest.php file is where we can use "uses()"function to bind different classes or traits, extend the API, and expose helpers as global functions. Which can allow us to reduce the number of lines of code in our test files. The Pest.php is automatically loaded.

$ php artisan pest:install

Laravel comes with example test files by default which is based on PHPUnit. Let's change the example test to use Pest instead. Navigate to the tests/Feature directory and look at the ExampleTest.php file. Below is what it currently look like:

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

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

        $response->assertStatus(200);
    }
}

Modify this to Pest implementation by replacing the content of the file with the code below:

<?php

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

I believe you will agree that this is easier, yes? The ExampleTest file has been refactored from about 12 lines of code to just 2 lines while testing the same thing and producing the same result. This is how easy and exciting Pest can be.

This code only checks that visiting the root URL '/' returns an HTTP status code of 200.

Let's also make the ExampleTest.php file located in the tests/Unit directory use Pest. Replace the contents of the ExampleTest.php file in the tests/Unit with the code below:

<?php

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

Now, use the below command to run the test:

$ ./vendor/bin/pest

All the tests should pass, as in the image below.

Configure the database

I'll use MySQL database for this, but you can use any other database you prefer.

DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=pest_test DB_USERNAME=root DB_PASSWORD=

Let's Create the post Model, Migration and Controller

Our post-application is going to have a Model, Migration, and Controller. Let's use the Laravel command below to generate all at the same time:

$ php artisan make:model Post -m -c

A new Post.php file is created for us. Let's edit it by opening the App/Model/Post.php file and updating it with the code below:

<?php

namespace App\Models;

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

class Post extends Model
{
    use HasFactory;
    protected $fillable = ['title','excerpt','content'];
}

The $fillable above is simply mass-assigning the table fields that can be filled, which includes title, excerpt, and content attribute of the model.

A new migration file is also created for us in the database/migrations directory at [TODAYSDATE]_create_poststable.php. Now, let's add the below line of code to the up() method within the migration file and run the migration.

$table->id();
$table->string('title');
$table->longText('excerpt')->nullable();
$table->boolean('content');
$table->timestamps();
php artisan migrate

This will create a posts table in our database with id, title, excerpt, content, created_at, and updated_at

Create the post factory

Laravel's factories, is a simple way of seeding data into the database. This is handy for testing purposes. Run the command below to create a factory class for the post model:

$ php artisan make:factory PostFactory

This creates PostFactory.php for us in the database/factories directory. Edit the definition() method within the file to return an array like the one below:

 return [
            "title" => "talentql pipeline program is top notch",
            "excerpt" => "Building on the success of its technical outsourcing and recruitment service",
            "content" => "The program, which is focused on training and upskilling mid-level software engineers across the continent to Senior Software Engineers",
        ];

This set's the default attribute values applied when creating a model using the factory.

Lets write our tests

At this point, let's get our hands soiled by writing some tests. Run the below Pest command to create a unit test file:

$ php artisan pest:test PostTest --unit

This will create PostTest.php in the tests/Unit directory. Navigate to test/Unit/PostTest.php and replace the code in the file with the below code:

<?php

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

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

it('can create a post', function () {
    $post = Post::factory()->raw();
    $response = $this->postJson('/api/posts', $post);
    $response->assertStatus(201)->assertJson(['message' => 'post created successfully', 'status' => 'success']);
    $this->assertDatabaseHas('posts', $post);
});

it(‘can create a post’) - This test makes sure that a post can be created by making a POST request to the API/posts endpoint. Here we assert that the HTTP status code returned is 201. We use the assertDatabase() method to ensure that the post exists in the database.

To run the test, run the below command:

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

Our test will fail. You ask why? Because we have not implemented the route and controller to handle the request. The test will fail with a 404 error, as in the image below.

We will finish writing all our test cases before implementing the Route and Controller functions necessary to pass.

Copy the below code and replace it with the content of test/Unit/PostTest.php:

<?php

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

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

it('can create a post', function () {
    $post = Post::factory()->raw();
    $response = $this->postJson('/api/posts', $post);
    $response->assertStatus(201)->assertJson(['message' => 'post created successfully', 'status' => 'success']);
    $this->assertDatabaseHas('posts', $post);
});

it('error when title not supplied', function () {
    $response = $this->postJson('/api/posts', ['title' => 'Updated tite']);
    $response->assertStatus(422);
});

it('error when content not supplied', function () {
    $response = $this->postJson('/api/posts', ['title' => 'Updated tite']);
    $response->assertStatus(422);
});

it('error when title and content are not supplied', function () {
    $response = $this->postJson('/api/posts', []);
    $response->assertStatus(422);
});

it('can fetch a post', function () {
    $post = Post::factory()->create();
    $response = $this->getJson("/api/posts/{$post->id}");
    $data = [
        'message' => 'post fetched successfully',
        'data' => [
            'id' => $post->id,
            'title' => $post->title,
            'excerpt' => $post->excerpt,
            'content' => $post->content,
        ]
    ];

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

it('can not fetch a post with wrong id', function () {
    $post = Post::factory()->create();
    $wrongId = $post->id + 1;
    $response = $this->getJson("/api/posts/{$wrongId}");
    $response->assertStatus(500);
});

it('can update a post', function () {
    $post = Post::factory()->create();
    $updatedpost = ['title' => 'Updated tite', 'content' => 'Updated content'];
    $response = $this->putJson("/api/posts/{$post->id}", $updatedpost);
    $response->assertStatus(200)->assertJson(['message' => 'post updated successfully']);
    $this->assertDatabaseHas('posts', $updatedpost);
});

it('can delete a post', function () {
    $post = Post::factory()->create();
    $response = $this->deleteJson("/api/posts/{$post->id}");
    $response->assertStatus(200)->assertJson(['message' => 'post deleted successfully']);
    $this->assertCount(0, Post::all());
});

The uses() method at the top of the file 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 migrates and resets the database after each test so that data from a previous test does not interfere with subsequent tests.

To run the tests, run the command below:

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

The tests should be failing as we still have not implemented the Route and Controller functions yet.

The Route and Controller

Below are the endpoints we will need in our post-application.

  • The create() method creates a new post.
  • The get() method returns a given post by ID.
  • The update() method updates a particular post.
  • The delete() method deletes a particular post.

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

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;


Route::post("/posts", "App\Http\Controllers\PostController@create");
Route::get("/posts/{id}", "App\Http\Controllers\PostController@get");
Route::update("/posts/{post}", "App\Http\Controllers\PostController@update");
Route::delete("/posts/{post}", "App\Http\Controllers\PostController@delete");

Let's supply the complementary implementation of the tests we've written thus far. Head over to the PostController.php file in the app/Http/Controller directory and replace the code with the following code:

<?php

namespace App\Http\Controllers;

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

class PostController extends Controller
{
    protected function rules()
    {
        return [
            'title' => 'required|string',
            'content' => 'required|string'
        ];
    }

    protected function ResponseMap($post)
    {
        return [
            'id' => $post->id,
            'title' => $post->title,
            'excerpt' => $post->excerpt,
            'content' => $post->content
        ];
    }

    public function create(Request $request)
    {
        $request->validate($this->rules());
        $post = Post::create($request->all());

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

    public function show($id)
    {
        $post = Post::find($id);
        return response()->json([
            'status' => 'success',
            'message' => 'post fetched successfully',
            'data' => $this->ResponseMap($post)
        ]);
    }

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

        $post->update($request->all());
        $post->refresh();

        return response()->json([
            'status' => 'success',
            'message' => 'post updated successfully',
            'data' => $this->ResponseMap($post)
        ]);
    }

    public function delete(Post $post)
    {
        $post->delete();

        return response()->json([
            'status' => 'success',
            'message' => 'post deleted successfully',
            'data' => $this->ResponseMap($post)
        ]);
    }


}

We've provided all the complementary implementations for the tests. We can now run our tests, and they should pass now. Run the test case with the following command:

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

Conclusion

We, now dear to say testing is simple and fun again. We've seen how to write unit tests for a Laravel application using the Pest framework. This article can serve as an excellent guide for getting started with Pest and unit testing a Laravel application. The GitHub repository with the complete code for this project can be found here.