[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 suitephp 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
- PHPUnit for unit tests on models/controllers
- Laravel Dusk for full E2E browser tests
- Pa11y for automated accessibility checks in CI
- 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!