DEV Community

Amruta
Amruta

Posted on

Guide to Test-Driven Development (TDD) in Laravel: A Step-by-Step Example

Test-Driven Development (TDD) is a methodology where tests are written before the actual code. This approach ensures that every piece of your application works as expected, and it helps prevent bugs early in the development process. In this guide, we’ll explore how to implement TDD in Laravel with a real-world example.

What is TDD?

TDD follows a simple cycle called Red-Green-Refactor:

  1. Red – Write a test for a new feature and watch it fail (since no code has been written yet).
  2. Green – Write the minimum amount of code required to pass the test.
  3. Refactor – Improve the code without changing its behaviour.

Setting Up Laravel for Testing

Before jumping into writing tests, you need to set up Laravel's testing environment. Laravel uses PHPUnit, which comes pre-installed.

  1. Install Laravel: If you haven’t already installed Laravel, you can do so with the following command:
   composer create-project --prefer-dist laravel/laravel tdd-example
Enter fullscreen mode Exit fullscreen mode
  1. Set Up a Testing Database: By default, Laravel uses SQLite for testing, but you can configure other databases if needed.

In .env.testing, define a separate testing database:

   DB_CONNECTION=sqlite
   DB_DATABASE=:memory:
Enter fullscreen mode Exit fullscreen mode
  1. Run Tests: Laravel’s default testing setup includes some example tests. You can run them using:
   php artisan test
Enter fullscreen mode Exit fullscreen mode

You should see an output showing which tests have passed or failed.


Building a Feature Using TDD: Todo List Example

Let's walk through building a simple "To-Do List" feature using TDD.

1. Write the Test (Red Phase)

Our goal is to create a Todo model, and we want to ensure users can add a new item to the list.

Let's write the test first:

php artisan make:test TodoTest
Enter fullscreen mode Exit fullscreen mode

In tests/Feature/TodoTest.php, write the following test:

<?php

namespace Tests\Feature;

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

class TodoTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function a_todo_can_be_created()
    {
        $response = $this->post('/todos', [
            'title' => 'Buy groceries',
        ]);

        $response->assertStatus(201);
        $this->assertCount(1, Todo::all());
    }
}
Enter fullscreen mode Exit fullscreen mode

This test checks:

  • Whether we can send a POST request to /todos to create a new to-do item.
  • If the item is successfully created, it asserts that the response status is 201.
  • It checks the database to ensure a new item has been added.

Now, run the test:

php artisan test
Enter fullscreen mode Exit fullscreen mode

The test will fail since we haven't written the actual functionality yet. This is the "Red" phase.

2. Write the Code (Green Phase)

Next, we’ll write the minimum amount of code to make the test pass.

  1. Create the Todo Model:
   php artisan make:model Todo -m
Enter fullscreen mode Exit fullscreen mode

This will generate a Todo model and a migration file.

  1. Update the Migration: In database/migrations/xxxx_xx_xx_create_todos_table.php, modify the migration:
   public function up()
   {
       Schema::create('todos', function (Blueprint $table) {
           $table->id();
           $table->string('title');
           $table->timestamps();
       });
   }
Enter fullscreen mode Exit fullscreen mode

Then, run the migration:

   php artisan migrate
Enter fullscreen mode Exit fullscreen mode
  1. Define the Route and Controller: Now, we’ll create the route and controller for handling the POST request.

In routes/web.php:

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

   Route::post('/todos', function (Request $request) {
       Todo::create($request->validate([
           'title' => 'required',
       ]));

       return response()->json([], 201);
   });
Enter fullscreen mode Exit fullscreen mode

Ensure the Todo model is mass-assignable by adding the $fillable attribute to app/Models/Todo.php:

   protected $fillable = ['title'];
Enter fullscreen mode Exit fullscreen mode
  1. Re-run the Test: Now that we’ve written the code to handle the creation of a todo, rerun the test:
   php artisan test
Enter fullscreen mode Exit fullscreen mode

If everything is correct, the test should pass. This is the "Green" phase.

3. Refactor the Code (Refactor Phase)

With the test passing, now is the time to clean up the code or make improvements. Since this example is fairly simple, we don’t have much to refactor, but in real-world projects, you might extract methods, rename variables, or optimize queries at this stage.


Writing More Tests: Expanding the Todo Feature

Once the basic functionality is complete, you can continue adding more features with TDD. For example:

Test for Viewing All Todos

You might want a test that ensures users can view all the to-do items:

/** @test */
public function todos_can_be_fetched()
{
    Todo::factory()->create(['title' => 'First task']);
    Todo::factory()->create(['title' => 'Second task']);

    $response = $this->get('/todos');

    $response->assertStatus(200);
    $response->assertJsonCount(2);
}
Enter fullscreen mode Exit fullscreen mode

In routes/web.php, add the GET route:

Route::get('/todos', function () {
    return Todo::all();
});
Enter fullscreen mode Exit fullscreen mode

Test for Deleting a Todo

Write a test for deleting an existing to-do item:

/** @test */
public function a_todo_can_be_deleted()
{
    $todo = Todo::factory()->create();

    $response = $this->delete('/todos/' . $todo->id);

    $response->assertStatus(200);
    $this->assertCount(0, Todo::all());
}
Enter fullscreen mode Exit fullscreen mode

Update routes/web.php with the route for deletion:

Route::delete('/todos/{id}', function ($id) {
    Todo::findOrFail($id)->delete();
    return response()->json([], 200);
});
Enter fullscreen mode Exit fullscreen mode

Benefits of TDD in Laravel

  • Fewer Bugs: Since you’re writing tests before the code, you catch issues early.
  • Confidence: You can refactor your code without fear of breaking functionality.
  • Documentation: Tests serve as documentation by demonstrating how your code is intended to work.

Conclusion

Test-driven development is a powerful practice that ensures your Laravel applications are robust and maintainable. By following the Red-Green-Refactor cycle, you write better, more reliable code. This guide walked you through a simple example of implementing TDD in Laravel, but the principles apply to more complex applications.

By writing tests first, you develop your applications with confidence, knowing that every new feature is thoroughly validated.

Top comments (0)