Writing Meaningful Tests with Pest PHP: Patterns That Scale

Move beyond "does it 200" testing. Learn architecture testing, dataset providers, custom expectations, and mutation testing with Pest PHP.

SE

SenpaiDev

Author

| | 4 min read | 2 |
Original article Updated Apr 27, 2026 Editorial standards

Pest PHP makes testing enjoyable, but most tutorials only cover the basics. These patterns will help you write a test suite that genuinely catches regressions, documents behavior, and stays fast as your application grows.

Architecture Tests: Enforce Your Standards Automatically

Pest's architecture testing plugin lets you encode your project conventions as tests. These tests run in milliseconds and prevent entire categories of structural issues:

// In tests/Arch.php
arch('controllers never directly query the database')
    ->expect('App\Http\Controllers')
    ->not->toUse(['Illuminate\Support\Facades\DB', 'Illuminate\Database\Eloquent\Model']);

arch('models use strict types')
    ->expect('App\Models')
    ->toUseStrictTypes();

arch('all action classes are invokable')
    ->expect('App\Actions')
    ->toBeInvokable()
    ->toHaveMethod('__invoke');

arch('no debug functions left in production code')
    ->expect(['dd', 'dump', 'ray', 'var_dump'])
    ->not->toBeUsed();

Datasets for Data-Driven Tests

When the same logic applies to multiple inputs, use datasets instead of duplicating tests:

dataset('invalid emails', [
    'missing @ symbol'  => ['notanemail'],
    'missing domain'    => ['user@'],
    'empty string'      => [''],
    'with spaces'       => ['user @example.com'],
]);

it('rejects invalid email addresses', function (string $email) {
    $response = $this->postJson('/api/register', [
        'email'    => $email,
        'password' => 'securepassword123',
    ]);

    $response->assertUnprocessable()
             ->assertJsonValidationErrors(['email']);
})->with('invalid emails');

This generates 4 separate test cases with clear names, runs them all, and makes it trivial to add new cases later.

Custom Expectations for Domain Logic

Create reusable expectations that make your tests read like business requirements:

// In tests/Pest.php
expect()->extend('toBePublished', function () {
    return $this->toHaveProperty('is_published', true)
                ->and($this->value->published_at)->not->toBeNull();
});

expect()->extend('toBelongTo', function (User $user) {
    return $this->toHaveProperty('user_id', $user->id);
});

// Usage in tests
it('publishes a blog post', function () {
    $post = Blog::factory()->create(['is_published' => true, 'published_at' => now()]);

    expect($post)->toBePublished()->toBelongTo($post->author);
});

Testing Livewire Components with Pest

Livewire's testing API integrates perfectly with Pest's fluent style:

it('searches blog posts in real time', function () {
    Blog::factory()->published()->create(['title' => 'Laravel Testing Guide']);
    Blog::factory()->published()->create(['title' => 'Vue.js Tutorial']);

    Livewire::test(BlogIndex::class)
        ->set('search', 'Laravel')
        ->assertSee('Laravel Testing Guide')
        ->assertDontSee('Vue.js Tutorial');
});

it('creates a blog post and notifies subscribers', function () {
    $admin = User::factory()->admin()->create();
    Notification::fake();

    Livewire::actingAs($admin)
        ->test(BlogCreate::class)
        ->set('title', 'New Post')
        ->set('content', str_repeat('word ', 100))
        ->call('save')
        ->assertRedirect();

    Notification::assertSentTo(
        User::where('id', '!=', $admin->id)->get(),
        NewBlogPostNotification::class
    );
});

Higher-Order Tests for Repetitive Auth Checks

Use Pest's higher-order testing to check multiple authorization scenarios concisely:

test('admin-only routes reject unauthenticated requests')
    ->get('/admin/blog/create')->assertRedirect('/login');

test('admin-only routes reject regular users')
    ->actingAs(User::factory()->create())
    ->get('/admin/blog/create')->assertForbidden();

Mutation Testing with Infection

Standard coverage metrics tell you which lines were executed. Mutation testing tells you if your tests actually verify the behavior. Install Infection and run it periodically to find tests that pass even when the code is deliberately broken. A test suite with 80% mutation score is far more valuable than 100% line coverage.

Great tests are an investment. They slow you down slightly today and pay back tenfold when you refactor confidently, catch regressions before deployment, and onboard new developers who can read tests to understand business rules.

Laravel field notes

How To Apply This In A Real Laravel App

Use the article as a starting point, then validate the idea against the shape of your application. In Laravel projects, the safest pattern is to make the first version small, measurable, and easy to remove if the tradeoff is wrong.

Implementation approach

Start with one route, one controller or action, and one test that proves the expected behavior. Once the path is stable, extract shared code into a service class or action only if a second caller needs it.

For production work, keep config in environment variables, cache expensive reads, and add clear failure states. A feature that works locally but fails silently in a queue, scheduler, or cached config environment is not ready for users.

Review Checklist

  • Add a feature or regression test before changing shared behavior.
  • Run the route through production-like cache settings with config and route caching enabled.
  • Check authorization, validation, and error responses before exposing the feature publicly.
  • Document any non-obvious tradeoff in the code or article notes so future edits stay honest.
SE

Written by

SenpaiDev

Developer and publisher at SenpaiDev, writing practical notes on Laravel, PHP, browser tools, and shipping better web products.

Comments (0)

Join the conversation

Log in to comment

No comments yet. Be the first to share your thoughts!

Newsletter

Get useful digital tips in your inbox

Get practical guides on files, privacy, productivity, writing, online tools, and web work. No spam, no daily blasts, just useful updates.

No spam, unsubscribe anytime. We respect your privacy.