php elephant sticker
Photo by RealToughCandy.com on Pexels.com

[Intermediate] Complete Guide to Building a Laravel App with Test-Driven Development (TDD)

What You’ll Learn

  • Basics of unit testing models and controllers with PHPUnit
  • How to set up end-to-end (E2E) tests using Laravel Dusk
  • Introducing automated accessibility tests with Pa11y
  • Automating tests in your CI/CD pipeline
  • Practical sample code and project structure recommendations

Who This Is For

  • Mid-level engineers who haven’t yet systematically learned unit or E2E testing
  • Team leads aiming to improve code quality by adopting TDD
  • Developers who want to automate accessibility checks and build inclusive services

Accessibility Level: ★★★☆☆

PHPUnit/Dusk tests will verify the existence of focusable elements; Pa11y will check contrast ratios and label presence


1. Introduction: Why TDD Works So Well in Laravel

Test-Driven Development (TDD) follows the cycle: Write a test → Write code → Refactor.
This approach catches bugs early and keeps your codebase maintainable.
Laravel’s built-in testing support—via PHPUnit and Dusk—makes writing tests effortless.

  • Quality Assurance: Prevent regressions while adding features
  • Living Documentation: Tests serve as usage examples
  • Confidence: Run tests in CI/CD to catch issues before merging

In this guide, we’ll walk through the full TDD workflow: PHPUnit unit tests → Dusk E2E tests → Pa11y accessibility tests → CI integration.


2. Environment Setup: Installing PHPUnit, Dusk, and Pa11y

First, prepare your Laravel project:

# PHPUnit comes pre-installed with Laravel
php artisan test --parallel

# Install Dusk for browser tests
composer require --dev laravel/dusk
php artisan dusk:install
  • php artisan test runs your PHPUnit suite
  • php artisan dusk runs browser-based Dusk tests

Then install Pa11y for accessibility checks:

npm install --save-dev pa11y

You’re now ready to write automated accessibility tests, too!


3. Unit Tests: PHPUnit for Models and Controllers

3.1 Model Test Example

Test the published scope on a Post model:

// tests/Unit/PostTest.php
namespace Tests\Unit;

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

class PostTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function it_only_returns_published_posts()
    {
        Post::factory()->count(3)->create(['published' => true]);
        Post::factory()->count(2)->create(['published' => false]);

        $published = Post::published()->get();

        $this->assertCount(3, $published);
    }
}
  • RefreshDatabase resets migrations between tests
  • /** @test */ tells PHPUnit to treat the method as a test

3.2 Controller Test Example

Test the store endpoint of PostController:

// tests/Feature/PostControllerTest.php
namespace Tests\Feature;

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

class PostControllerTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function guests_cannot_create_posts()
    {
        $response = $this->postJson('/api/posts', [
            'title' => 'Test',
            'body'  => 'Content',
        ]);

        $response->assertStatus(401);
    }

    /** @test */
    public function authenticated_users_can_create_posts()
    {
        $user = User::factory()->create();
        $this->actingAs($user, 'sanctum');

        $response = $this->postJson('/api/posts', [
            'title' => 'Hello',
            'body'  => 'World',
        ]);

        $response->assertStatus(201)
                 ->assertJson(['title' => 'Hello']);
    }
}
  • Use postJson for API requests, assert status codes and JSON structure

4. E2E Tests: Automating UI Flows with Laravel Dusk

Dusk shines for browser-based flows. Example:

// tests/Browser/LoginTest.php
namespace Tests\Browser;

use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use App\Models\User;

class LoginTest extends DuskTestCase
{
    /** @test */
    public function user_can_login_via_login_page()
    {
        $user = User::factory()->create(['password' => bcrypt('secret')]);

        $this->browse(function (Browser $browser) use ($user) {
            $browser->visit('/login')
                    ->type('email', $user->email)
                    ->type('password', 'secret')
                    ->press('Login')
                    ->assertPathIs('/dashboard')
                    ->assertSee('Welcome');
        });
    }
}
  • Simulate form input, button clicks, and page navigation
  • Use assertSee to verify visible text (also aids accessibility checks)

5. Automated Accessibility Tests with Pa11y

Integrate Pa11y into your npm scripts:

// package.json
"scripts": {
  "test:a11y": "pa11y http://localhost:8000 --reporter html > a11y-report.html"
}

Run:

npm run test:a11y
  • Checks contrast ratios, missing labels, ARIA attributes
  • Outputs an HTML report for CI integration

6. CI/CD Integration: GitHub Actions Example

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  tests:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8
        env:
          MYSQL_ROOT_PASSWORD: password
        ports:
          - 3306:3306
    steps:
      - uses: actions/checkout@v2
      - uses: shivammathur/setup-php@v2
        with:
          php-version: 8.1
      - run: composer install --prefer-dist
      - run: cp .env.example .env
      - run: php artisan key:generate
      - run: php artisan migrate --force
      - run: php artisan test
      - run: npm install
      - run: npm run test:a11y
  • The pipeline fails if any test (unit, Dusk, or accessibility) fails—ensuring code quality before merge

7. Conclusion: High-Quality, Accessible Laravel Development with TDD

  1. PHPUnit for unit tests on models/controllers
  2. Laravel Dusk for full E2E browser tests
  3. Pa11y for automated accessibility checks in CI
  4. GitHub Actions (or similar) to enforce tests before deployment

Adopting TDD helps you deliver bug-free, maintainable code and build web services everyone can access. Give this guide a try on your next Laravel project!

By greeden

Leave a Reply

Your email address will not be published. Required fields are marked *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)