Test Driven Development (TDD) in application development in Laravel

Video thumbnail

If you already work with Laravel and have used unit or integration tests with PHPUnit or Pest, sooner or later an inevitable question arises:

Does applying TDD make sense or is it just another fad?

I didn't understand the true value of Test Driven Development (TDD) until I started using tests as a system specification, not just as a final verification. And Laravel, because of how it is designed, fits perfectly with this way of working.

In this article, I explain what TDD is, how it works in Laravel, and above all, how to apply it with a real example—even when the response is not a JSON but a Blade view with pagination—and why it makes sense to use these types of techniques when developing; especially since we already know how to use Unit and Integration Tests with PHPUnit and Pest in Laravel.

What is TDD and why does it make sense in Laravel?

As an additional consideration, we are talking about a technique called Test Driven Development (TDD), which is a programming practice where tests are written before creating the functionality code, following these key aspects:

  1. By defining tests first, it allows specifying each functionality before writing the actual code; it's like a kind of mental tree, but with code, thus guiding the development process.
  2. Clean and robust code: The goal is to create clean, robust, and simple code. If the tests fail, errors are fixed before moving forward.

Test Driven Development (TDD) is a technique in which you write the tests first and then the functionality code. Not the other way around.

The key idea is not just "having tests," but having the tests define the system's behavior. Before writing a single line of logic, you already know:

  • Which route should exist
  • What data it should return
  • What format they should have
  • What happens if something fails

Laravel greatly facilitates this approach because it comes prepared from the very first moment to work with tests, whether at the level of routes, controllers, database, or views.

When you already know PHPUnit or Pest, TDD stops being something abstract and becomes a very clear way of thinking about design before programming.

The TDD Cycle: Red, Green, and Refactor

The heart of TDD is a very simple but powerful cycle that repeats constantly:

Red: write a failing test

First, you write a test for a functionality that doesn't exist yet.
The test fails, and that is exactly what we are looking for.

Green: write the minimum code

Then you write only the code necessary for the test to pass.
No more. No less.

Refactor: improve without breaking anything

Once the test is green, you refactor with peace of mind, knowing that if something breaks, the tests will tell you.

This cycle completely changes the way you develop. Instead of "programming and then testing," you move to thinking about the expected result first.

TDD in Laravel: tools and types of tests

Laravel is designed with testing in mind and offers several key tools:

  • PHPUnit: the classic testing framework in PHP.
  • Pest: a modern and expressive layer on top of PHPUnit.
  • Artisan: commands like php artisan test or make:test.
  • Isolated testing environment: in-memory database, sessions, cache, etc.

Additionally, Laravel clearly differentiates between:

  • Unit Tests: small, isolated logic.
  • Feature Tests: complete HTTP flows (routes, views, controllers).

For TDD in Laravel, most real-world cases are handled very well with Feature tests because they allow defining the complete behavior of a functionality.

Example to evaluate Test Driven Development

To give an example of the importance of testing in software development and, of course, how it applies to Laravel development.

The paginated list test will be similar to the one performed in the REST API, but since the evaluation of the response is not a JSON but a paginated list, specific methods are used for that purpose.

Many guides focus on APIs and JSON responses, but in real projects, we continue to work with Blade views, HTML, and paginated data. And that can also be defined with TDD.

In a real test I implemented, the goal was clear:

a route that shows a paginated list of posts in a specific view.

Writing the test first

tests\Feature\Web\BlogTest.php

<?php
namespace Tests\Feature\Web;

use App\Http\Controllers\blog\BlogController;
use App\Models\Post;
use Illuminate\Foundation\Testing\DatabaseMigrations;

use Illuminate\Pagination\LengthAwarePaginator;
use Tests\TestCase;

class BlogTest extends TestCase
{
    use DatabaseMigrations;

    function test_index()
    {
        // $response = $this->get('/blog')
          $this
            ->get(route('blog.index'))
            ->assertStatus(200)
            ->assertViewIs('blog.index')
            ->assertSee('Post List')
            ->assertViewHas('posts', Post::paginate(2));
           $this->assertInstanceOf(LengthAwarePaginator::class,$response->viewData('posts'));
    }
}

Before implementing anything, this test already defines all the expected behavior:

  • A named route blog.index exists
  • It returns HTTP 200
  • It renders the blog.index view
  • It contains the text “Post List”
  • It sends a paginated posts variable to the view

This is where I truly understood that the test is the design.

Key assertions in Laravel for TDD

Laravel offers very powerful assertions that fit perfectly with TDD:

In this test, we will see several interesting aspects; for variety, we show that we can also use a named route:

->get(route('blog.index'))

assertViewIs()

With this assertion method, we verify by the name of the view, along with its path:

->assertViewIs('blog.index')

assertViewHas()

With this method, we verify by the data supplied to the view and its name, which in this case is posts, which is the paginated list for the posts:

->assertViewHas('posts', Post::paginate(2));

With the following assertion method, we obtain the data from the view:

$response->viewData('posts')

And we verify that it is an instance of a class, since we are using:

Post::paginate(2)

Validating the data type

We know it is of type LengthAwarePaginator:

$this->assertInstanceOf(LengthAwarePaginator::class,$response->viewData('posts'));

This test, which is our first real test on the Laravel app that returns a view—meaning HTML content generated with Blade and not something as simple or flat as a JSON—allows us to see more specific assertion methods to ensure the data has the expected format.

As you can see, these tests also serve to specify where and how elements like data, view, and route should be composed; therefore, by specifying a clear structure, techniques like TDD make sense. In short, when developing a new project, you start first with the tests, and it is the tests that specify what should be implemented at the functional level.

What TDD brings to real projects

After applying TDD this way, the benefits become very evident:

  • ✔️ Simpler code
    • If the test passes, the code is sufficient. There is no over-engineering.
  • ✔️ Better design
    • Tests force you to separate responsibilities and think in clear contracts.
  • ✔️ Confidence when refactoring
    • Changing code stops being scary because tests act as a safety net.
  • ✔️ Living documentation
    • Tests explain how the application should behave better than any README.

In my experience, when you start a project directly with TDD, technical decisions are much more coherent from day one.

Conclusion

TDD in Laravel is not about writing more tests, but about using tests as a development guide.
When tests define routes, views, data, and behavior, development becomes clearer, safer, and more maintainable.

If you already know PHPUnit or Pest, making the leap to TDD is more natural than it seems. And as you've seen, it's not just for APIs, but also for Blade views, pagination, and real web application flows.

The next natural step is to delve deeper into validations, permissions, and roles—for example using Spatie Permissions—always keeping tests as the starting point.

The next step is to learn how to use Spatie and permissions in Laravel.

Discover how to apply TDD in Laravel step by step with real examples, Feature tests, PHPUnit, Pest, and Blade view validation with pagination.

I agree to receive announcements of interest about this Blog.

Andrés Cruz

ES En español