Why don't you start writing tests?

See how easy it is to get started with automated testing in Laravel

Why don't you start writing tests?

There are hundreds of articles on why you should write tests for your code. And I assume you have read them many times.

Many developers realize the importance of writing tests but just do not get started.

Why Not

Mainly because of some misunderstanding.

  • They face the fear of doing it wrong.
  • They think they're not good enough (yet) to write tests.
  • They are not sure which type of tests apply to their projects.

As someone who has been there, let me tell you these things are temporary. Once you start writing tests, all this will go away.

Why Not Today

If I were to tell you that it would take only 5-10 minutes of your time to get started with testing, would you mind starting now?

Let today be the start of something new.

  • William Shakespeare

Okay. So let's kick off our testing journey with a Laravel application. You may use an existing web application or create a new one.

In fact, you can follow this article even for any other programming language/framework as we are going to discuss the high-level stuff only.

Default tests with Laravel

Laravel comes with a couple of tests out of the box. Open your terminal and type:

php artisan test

And if you haven't messed anything up in your web app, you would be greeted with something like:

First tests with Laravel

Great, we have a perfect starting point. Let's write our own tests now.

Some web app code

If you have a fresh new Laravel web application, please append the following code to the routes/web.php file before writing your first test.

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

// existing code...

Route::post('/users', function (Request $request) {
    $validated = $request->validate([
        'name' => ['required', 'string', 'max:255'],
        'email' => ['required', 'email', 'max:255'],
        'password' => ['required', 'confirmed'],
    ]);

    User::create($validated);

    return back()->with('success', 'User created successfully.');
});

The following things are happening in this code:

  1. We validate the form data.
  2. We create the user in the database.
  3. We redirect the user back with a success message.

And our tests should cover all these.

Create a new test file

We'll start by creating a new test file. Please execute the following command in your terminal:

php artisan make:test UserTest

A new test file should be created at tests/Feature/UserTest.php.

Heads up - Resetting the database

If your code and tests play with the database, you should reset your database after each of your tests to ensure that one test cannot affect the results of another one. Laravel makes it effortless by providing a trait that you can add to your test classes.

Please add the following trait in the UserTest.php file:

use Illuminate\Foundation\Testing\LazilyRefreshDatabase;

class UserTest extends TestCase
{
    use LazilyRefreshDatabase;

    // existing code...

With that done, let's bake tests.

Your first test - the Happy path

The default test file already covers a test for a GET request. Thus, we can directly write a test for a POST request.

When the user submits the form from the browser, a POST request is made with the inputs and values. We are going to simulate the same thing in our tests.

Please append the following code to the UserTest.php file:

    // existing code...

    public function test_new_user_can_be_added()
    {
        $response = $this->post('/users', [
            'name' => 'Gaurav',
            'email' => 'gauravmakhecha@gmail.com',
            'password' => '123456',
            'password_confirmation' => '123456',
        ]);

        $response->assertRedirect('/')
            ->assertSessionHas('success', 'User created successfully.');

        $this->assertDatabaseHas('users', [
            'name' => 'Gaurav',
            'email' => 'gauravmakhecha@gmail.com',
        ]);
    }

Laravel provides an easy way to make a POST request from your test files using the post() method. Using that, we make the request and verify that user details are added to the database and the response is a redirect with the proper message in the session.

Wasn't this straightforward? Let's try one more.

Test for the validation

How about adding a test to make sure that our validation works as expected? Please append the following code to the UserTest.php file:

    // existing code...

    public function test_new_user_data_is_validated()
    {
        $response = $this->post('/users', [
            'name' => '',
            'email' => 'not_email_format',
            'password' => '123456',
            'password_confirmation' => '456789',
        ]);

        $response->assertStatus(302)
            ->assertSessionHasErrors(['name', 'email', 'password']);
    }

Here, we pass invalid data for the form inputs and confirm (assert) that there is a redirect response (HTTP status code 302) and that there are validation errors in the session.

Test for the unique email address

The business requirement is that the email address of each user has to be unique so let us add a test for the same. Append this test to the UserTest.php file:

    // existing code...

    public function test_new_user_with_same_email_cannot_be_added()
    {
        User::factory()->create([
            'email' => 'gauravmakhecha@gmail.com',
        ]);

        $response = $this->post('/users', [
            'name' => 'Gaurav',
            'email' => 'gauravmakhecha@gmail.com',
            'password' => '123456',
            'password_confirmation' => '123456',
        ]);

        $response->assertStatus(302)
            ->assertSessionDoesntHaveErrors(['name', 'password'])
            ->assertSessionHasErrors(['email']);
    }

This test uses Laravel Factory to add a user to the database and then tries to make a POST request with the same email address. Let us run tests and see the results.

A failing test

Oops, it failed. And the reason? We missed adding the unique validation in our code.

The Fix

Now your test is guiding you with the development. (Yes TDD!)

Please make the following update in the routes/web.php file:

-        'email' => ['required', 'email', 'max:255'],
+        'email' => ['required', 'email', 'max:255', 'unique:users'],

Once updated, give yourself a pat on the back and run the tests again (php artisan test)

Passing tests

What more?

Did you realize you are now officially a developer who writes automated tests? Congrats!

Congrats!

Need more examples? Check this directory from the official Laravel repository for tests related to various modules.

The goal of this article is just to get you started with writing tests. Not to cover different use cases or scenarios. Hence, I limit the practical coding here and would like to share a few other things to help you progress with it.

Patterns that can help you

Many developers aren't sure for which parts of their code should they write tests. The simplest answer is: Try to write automated tests for all the functionality that you check while manually testing your web app.

In this example, we would have checked the DB entry, the validation messages, and the redirect while manually testing the web app. And that is what our automated tests covered.

Here are a couple of patterns to help you plan your tests:

1. Given-When-Then

2. Arrange-Act-Assert

Both are similar so you can use any of them. The idea is:

  1. First, you set the scene (Add existing DB records, log in a user, etc.)
  2. Then you take an action (Visit a URL, submit a form, etc.)
  3. Finally, you verify the results/effects (A DB entry, a session call, response status code, etc.)

This way can generally help you in writing most of the test cases.

Moreover, we have a dedicated article about writing tests only for your code to guide you around the test boundaries.

(Not so) Complex Stuff

What we covered in this article are Feature tests. There are many other types of tests (Unit tests, Feature tests, Integration tests, Acceptance tests, etc.).

Do not hurry to learn/use all of them, you don't have to. You will feel the need yourself if/when the time arrives.

Topics like mocking and stubbing fall in the same category. Believe me, it is not compulsory to use them. But keep in mind they are quite handy for medium to big-size projects. We use them in our projects and can't live without them.

A Note on PEST

We use PEST for writing tests but I decided not to include that in this article to keep things simple. I wish (and am sure) it would come out of the box with Laravel.

To The Moon

I hope this article gets you started with writing tests and you never stop in the future. Feel free to comment below in case of any questions. May those tools give you more testing features ๐Ÿ˜‰

And if any of your friends are also giving excuses to start writing tests, how about sharing this article with them for the push?

Cheers and regards.

ย