Testing in Laravel

Photo by CDC on Unsplash

Testing in Laravel

Laravel's robust framework caters to a developer's desire for efficiency and elegance. But what about assurance, the quiet confidence that your code won't crumble under real-world pressure? That's where testing becomes your knight in shining armor.

The Power of Tests

Testing isn't simply about catching bugs; it's about preventing them. By simulating user interactions and various scenarios, you proactively identify potential flaws, ensuring predictable, consistent behavior.

Laravel is built with testing in mind. In fact, support for testing with PHPUnit is included out of the box and a phpunit.xml file is already set up for your application. The framework also ships with convenient helper methods that allow you to expressively test your applications.

PHPUnit Testing tool

PHPUnit is one of the oldest and most well-known unit testing packages for PHP. It is primarily designed for unit testing, which means testing your code in the smallest components possible, but it is also incredibly flexible and can be used for a lot more than just unit testing. Moreover, it supports all major PHP frameworks including Laravel.

PHPUnit is developed with simple assertions, which make it pretty easier for you to test codes completely. Further it gives optimum results when you are testing code components individually, giving you magnified results, so that errors can be figure out easily. However, this means that testing much more advanced components like controllers, models and form validations can be a bit complicated as well.

In your newly installed Laravel application, you will find two files in the ./tests/ directory, one of ExampleTest.php and the other TestCase.php. TestCase.php file is basically a bootstrap file which sets the Laravel environment and features within our tests. It makes easy to use Laravel features in tests, and also enables framework for the testing helpers. The ExampleTest.php constitutes an example test class which contains basic test case using app testing helpers.

For creating new test class, you can either create a new file manually, or can run the inbuilt artisan make:test command of the Laravel.

Before running the test you should update the test environment in .env.testing file or phpunit.xml file.

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

Let's create a test by running this command on your terminal.
php artisan make:test BasicTest it will create a test file in ./tests/ directory.

<?php
# test/Feature/BasicTest.php

namespace Tests\Feature;

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

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

        $response->assertStatus(200);
    }
}

You can create multiple method for your test class, individual method behave like a individual test case. Okay, now run the tests.

php artisan test

You will see a output like this.

Given, When, Then in Testing

If you want to test particular functions of your code, then you should follow the intrinsic pattern of Given, When and Then.

Given – It states the initial environment setup you would like to test. You can use some data, or can even setup a model factory in this step.

When – When refers to test specific function/method and called at some particular stage of the testing process.

Then – In this part, you declare about the result how it should look like.

<?php 
// ...
    public function testStartsWithALetter()
    {
        # Given
        $box = new Box(['toy', 'torch', 'ball', 'cat', 'tissue']);

        # When
        $results = $box->startsWith('t');

        # Then
        $this->assertCount(3, $results);
        $this->assertContains('toy', $results);
        $this->assertContains('torch', $results);
        $this->assertContains('tissue', $results);

        // Empty array if passed even
        $this->assertEmpty($box->startsWith('s'));
    }

There are some common assertion helper in PHPUnit

  1. assertTrue()

  2. assertFalse()

  3. assertEquals()

  4. assertNull()

  5. assertNotNull()

  6. assertContains()

  7. assertCount()

  8. assertEmpty()

  9. assertSame()

  10. and etc, you can find all the assertion method here

Pest Testing tool

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.

Setting up Pest

Installing Pest PHP Testing Framework is a simple process that can be completed in just a few steps. Before you begin, make sure you have PHP 8.1+ or higher installed on your system.

The first step is to require Pest as a "dev" dependency in your project by running the following command on your command line.

composer require pestphp/pest --dev --with-all-dependencies

Secondly, you'll need to initialize Pest in your current PHP project. This step will create a configuration file named Pest.php at the root level of your test suite, which will enable you to fine-tune your test suite later.

./vendor/bin/pest --init

Finally, you can run your tests by executing the pest command.

./vendor/bin/pest

Let's create a Pest test by running this command.
php artisan make:test HomepageTest --pest

it will create a test file for Pest

<?php

test('example', function () {
    $response = $this->get('/');

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

Now update the test case like this.

<?php

it('can display the homepage', function () {
    $response = $this->get('/');

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

Now run the test by php artisan test or ./vendor/bin/pest you will see output like this below.

Each test usually consists of three steps:

  1. Prepare your test first (creating models from factories, other setup)

  2. Act – do the test, execute the code that is being tested

  3. Assert that the 'Act' step has had the intended consequences

use App\Models\Post;

it('can display a post on the homepage', function () {
    // Prepare
    $post = Post::factory()->create();

    // Act
    $response = $this->get(route('home'));

    // Assert
    $response->assertOk();
});

Imagine we have a ToDo model and we are going to test the API endpoints so a full API testing example below.

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

And the output for this test.

Full set of unit tests passing

Expectation API

Now comes the interesting part, where Pest really stands apart from PHPUnit. In the above examples we used the $this->assertSame(...) methods to do an assertion. Pest also offers the so-called Expectation API, where you can write tests as if they were plain English. Let's refactor the previous example to use expectations:

it('is a dummy test', function () {
    $post = Post::factory()->create(['title' => 'Hello']);

    $title = $post->title;

    expect($title)->toBe('Hello');
});

You can test your title also by writing this way,

it('is a dummy test', function () {
    $post = Post::factory()->create(['title' => 'Hello']);

    expect($post)
        ->title
        ->toBe('Hello');
});

// You can also chain multiple Higher Order expectations
it('is a dummy test', function () {
    $post = Post::factory()->create(['title' => 'Hello']);

    expect($post)
        ->title->toBe('Hello')
        ->body->toBeNull();
});

You can also write our own custom Pest expectation by using the expect()->extend(...) function. See the following example from the Pest docs:

expect()->extend('toBeWithinRange', function ($min, $max) {
    return $this
       ->toBeGreaterThanOrEqual($min)
       ->toBeLessThanOrEqual($max);
});

test('numeric ranges', function () {
    expect(100)->toBeWithinRange(90, 110);
});

Conclusion: A Code of Confidence

Your journey to mastering Laravel testing isn't just about learning tools; it's about cultivating a developer's superpower - the unwavering confidence that your code will shine. Each executed test becomes a brick in the fortress of your application, protecting it from the unpredictable winds of real-world use.

So, embrace the test-driven mantra. Let curiosity be your compass, experiment be your fuel, and the Laravel community your support system. As you navigate the exciting landscape of testing, remember that the true reward isn't just bug-free code, but the empowerment to build software that stands tall, whispers quality, and roars with reliability.

Take the first step today. Pick up your keyboard, write your first test, and watch your confidence soar as you master the art of Laravel testing.

Resources