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:
- Red – Write a test for a new feature and watch it fail (since no code has been written yet).
- Green – Write the minimum amount of code required to pass the test.
- 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.
- 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
- 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:
- Run Tests: Laravel’s default testing setup includes some example tests. You can run them using:
php artisan test
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
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());
}
}
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
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.
- Create the Todo Model:
php artisan make:model Todo -m
This will generate a Todo
model and a migration file.
-
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();
});
}
Then, run the migration:
php artisan migrate
- 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);
});
Ensure the Todo
model is mass-assignable by adding the $fillable
attribute to app/Models/Todo.php
:
protected $fillable = ['title'];
- Re-run the Test: Now that we’ve written the code to handle the creation of a todo, rerun the test:
php artisan test
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);
}
In routes/web.php
, add the GET route:
Route::get('/todos', function () {
return Todo::all();
});
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());
}
Update routes/web.php
with the route for deletion:
Route::delete('/todos/{id}', function ($id) {
Todo::findOrFail($id)->delete();
return response()->json([], 200);
});
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)