Laravel Testing with Pest 5 in 2026: TIA, Mocking and Interview Questions
Master Laravel testing best practices with Pest 5, Test Impact Analysis, Mockery, facade fakes and architecture tests. Covers unit tests, feature tests, mocking strategies and common interview questions for Laravel developers.

Laravel testing best practices have shifted significantly with Pest 5, the TIA (Test Impact Analysis) engine, and PHPUnit 13. Laravel 13 ships with first-class Pest support, making the testing experience more expressive and less verbose than raw PHPUnit. This guide covers the patterns that matter for production applications and technical interviews alike.
Pest 5 (built on PHPUnit 13) is the default testing framework for Laravel 13. It requires PHP 8.4+ and introduces the TIA engine, first-party AI agent verification, PHPStan integration, and time-balanced sharding out of the box.
Setting Up Pest 5 in a Laravel 13 Project
Every new Laravel 13 application scaffolds Pest automatically. For existing projects, the migration from Pest 4 takes minutes since PHPUnit 13 compatibility is the main consideration. The configuration lives in tests/Pest.php, where global traits and helper methods are registered.
use Illuminate\Foundation\Testing\RefreshDatabase;
pest()
->extend(Tests\TestCase::class)
->use(RefreshDatabase::class)
->in('Feature');This single file replaces the old CreatesApplication trait and base test class inheritance. The RefreshDatabase trait wraps each test in a database transaction, rolling back changes automatically.
use App\Models\User;
it('registers a new user with valid data', function () {
$response = $this->postJson('/api/register', [
'name' => 'Jane Doe',
'email' => 'jane@example.com',
'password' => 'SecurePass123!',
'password_confirmation' => 'SecurePass123!',
]);
$response->assertStatus(201)
->assertJsonStructure(['user' => ['id', 'name', 'email']]);
expect(User::where('email', 'jane@example.com')->exists())->toBeTrue();
});Pest's expect() API chains naturally with PHPUnit assertions. The assertJsonStructure call validates the response shape, while expect()->toBeTrue() confirms the database state. Both styles coexist without conflict.
Test Impact Analysis: The Pest 5 Headline Feature
The TIA engine is what makes Pest 5 transformative for large codebases. On the first run, Pest records which tests touch which files. Every subsequent run executes only the tests affected by your changes and replays cached results for everything else.
# First run: records baseline (requires PCOV or Xdebug)
php artisan test
# Subsequent runs: only affected tests execute
php artisan testLaravel Cloud's test suite, containing more than 19,000 tests, dropped from around three minutes to five seconds with TIA enabled. The engine understands more than PHP files: it detects changes in migrations, Blade templates, and shared JavaScript components.
TIA requires a coverage driver (PCOV or Xdebug) to record the baseline. For CI pipelines, the cached mappings persist between runs, making the performance gain cumulative.
Unit Tests vs Feature Tests in Laravel
The distinction between unit and feature tests in Laravel determines which parts of the framework are booted. Unit tests run without the application container, making them faster but limited to pure logic. Feature tests boot the full application, enabling HTTP calls, database queries and service resolution.
use App\Services\PriceCalculator;
describe('PriceCalculator', function () {
it('applies a percentage discount correctly', function () {
$calculator = new PriceCalculator();
// 20% off a 150.00 base price
$result = $calculator->applyDiscount(150.00, 20);
expect($result)->toBe(120.00);
});
it('rejects negative discount values', function () {
$calculator = new PriceCalculator();
expect(fn () => $calculator->applyDiscount(100.00, -5))
->toThrow(InvalidArgumentException::class);
});
});Unit tests target isolated classes with no external dependencies. The describe block groups related assertions, and Pest 5 supports nested describe blocks for complex test hierarchies.
Feature tests should represent the majority of a Laravel test suite. They catch integration bugs that unit tests miss, such as incorrect route middleware, missing validation rules, or broken Eloquent relationships. A common rule: if the code touches the database, HTTP layer or a facade, write a feature test.
Mocking Strategies with Facades and Mockery
Mocking in Laravel testing isolates the code under test from external dependencies. Laravel facades provide built-in fake implementations for queues, events, notifications, mail and storage. Mockery handles everything else.
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use App\Mail\OrderConfirmation;
use App\Jobs\ProcessPayment;
it('dispatches payment job and sends confirmation email', function () {
// Fake both Mail and Queue facades
Mail::fake();
Queue::fake();
$user = User::factory()->create();
$order = Order::factory()->for($user)->create([
'total' => 99.99,
'status' => 'pending',
]);
// Act: confirm the order via HTTP
$this->actingAs($user)
->postJson("/api/orders/{$order->id}/confirm")
->assertOk();
// Assert: payment job was dispatched with correct amount
Queue::assertPushed(ProcessPayment::class, function ($job) use ($order) {
return $job->order->id === $order->id
&& $job->order->total === 99.99;
});
// Assert: confirmation email was sent to the user
Mail::assertSent(OrderConfirmation::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
});Facade fakes intercept calls at the framework level, preventing actual emails from being sent or jobs from being dispatched. The closure-based assertions verify the exact data passed to each component.
Avoid mocking internal domain classes. Mock at the boundary: third-party APIs, mail, queues, filesystems. Over-mocking makes tests brittle and tightly coupled to implementation details.
For dependencies that are not facades, inject them via constructor and use Mockery:
use App\Services\PaymentGateway;
use App\Services\StripeClient;
it('charges the customer through the payment gateway', function () {
// Create a mock of the Stripe client
$stripeClient = Mockery::mock(StripeClient::class);
$stripeClient->shouldReceive('charge')
->once()
->with('cus_abc123', 5000, 'usd')
->andReturn(['status' => 'succeeded', 'id' => 'ch_xyz']);
// Bind the mock in the container
$this->app->instance(StripeClient::class, $stripeClient);
$gateway = app(PaymentGateway::class);
$result = $gateway->processCharge('cus_abc123', 50.00);
expect($result['status'])->toBe('succeeded');
});Binding the mock with $this->app->instance() replaces the real implementation for the duration of the test. Mockery verifies that charge was called exactly once with the expected arguments.
Architecture Testing with Pest Presets
Pest 5 includes architecture testing that enforces structural rules across the codebase. These tests run against the AST, not the runtime, making them extremely fast.
arch('controllers do not use Eloquent directly')
->expect('App\Http\Controllers')
->not->toUse('Illuminate\Database\Eloquent');
arch('services are final classes')
->expect('App\Services')
->toBeFinal();
arch('no debugging functions in production code')
->expect(['dd', 'dump', 'var_dump', 'ray'])
->not->toBeUsed();The first rule prevents controllers from querying the database directly, enforcing a service layer pattern. The second ensures services cannot be extended, reducing inheritance complexity. The third catches leftover debug statements before they reach production.
Laravel-specific presets are also available:
arch()->preset()->laravel();
arch()->preset()->security();
arch()->preset()->php();These three lines enforce dozens of rules: models extend the correct base class, controllers do not contain business logic, no insecure functions like eval() or md5() for hashing, and standard PHP conventions are followed.
Ready to ace your Laravel interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Mutation Testing to Verify Test Quality
Code coverage measures which lines execute during tests. Mutation testing goes further: it modifies source code and checks whether tests detect the change. If a mutation survives, the test suite has a gap.
# Run mutation testing on a specific class
php artisan test --mutate --class=App\\Services\\PriceCalculatorPest 5 introduces mutations like changing > to >=, removing return statements, and flipping boolean conditions. A mutation score below 80% usually indicates that tests verify outputs without checking edge cases.
// Example: this test has high coverage but low mutation score
it('calculates shipping cost', function () {
$cost = calculateShipping(weight: 5.0, zone: 'domestic');
// Only checks that the result is numeric
expect($cost)->toBeFloat();
});
// Improved: catches mutations by asserting the exact value
it('calculates domestic shipping for 5kg package', function () {
$cost = calculateShipping(weight: 5.0, zone: 'domestic');
// Exact assertion catches operator and value mutations
expect($cost)->toBe(12.50);
});The first test passes even if the calculation logic is completely wrong, as long as it returns a float. The second test fails immediately when a mutation changes the formula. Specific assertions produce higher mutation scores.
Database Testing Patterns and Factories
Laravel model factories generate realistic test data without manual array construction. Pest 5 combined with factories produces readable, maintainable data setup.
use App\Models\Article;
use App\Models\User;
it('publishes a draft article and updates the timestamp', function () {
$author = User::factory()->create(['role' => 'editor']);
$article = Article::factory()
->for($author, 'author')
->draft()
->create(['title' => 'Testing Best Practices']);
$this->actingAs($author)
->patchJson("/api/articles/{$article->id}/publish")
->assertOk()
->assertJsonPath('data.status', 'published');
$article->refresh();
expect($article->status)->toBe('published')
->and($article->published_at)->not->toBeNull()
->and($article->published_at->isToday())->toBeTrue();
});The draft() factory state sets default values for unpublished articles. Chained expect() assertions with ->and() read like natural language and fail with descriptive messages.
Run php artisan test --parallel to execute tests across multiple processes. Laravel automatically creates separate test databases for each process, preventing data conflicts. Pest 5's time-balanced sharding distributes tests by actual execution time rather than count, ensuring every CI shard finishes at the same moment.
Common Laravel Testing Interview Questions
Technical interviews for Laravel positions frequently probe testing knowledge. Here are the questions that appear most often, along with what a strong answer covers.
What is the difference between fake(), mock() and spy() in Laravel testing?
Facade fake() replaces the entire facade with an in-memory implementation (Mail::fake, Queue::fake). mock() via Mockery sets expectations before execution and fails if they are not met. spy() records interactions and allows assertions after execution, without setting upfront expectations. The choice depends on whether the test needs to verify behavior (mock), record interactions (spy), or prevent side effects (fake).
How does RefreshDatabase differ from DatabaseTransactions?
RefreshDatabase runs migrations once and wraps each test in a transaction. DatabaseTransactions assumes the database already has the correct schema and only wraps tests in transactions. RefreshDatabase is safer for CI pipelines where the database might not exist yet. DatabaseTransactions is faster when the schema is guaranteed to be current.
When should feature tests be preferred over unit tests?
Feature tests should cover any code that interacts with the HTTP layer, database, or Laravel services. Unit tests are reserved for pure functions and value objects with no framework dependencies. In a typical Laravel application, feature tests outnumber unit tests by a ratio of roughly 3:1 or higher. This reflects the reality that most Laravel code is inherently integrated with the framework.
How does Test Impact Analysis improve CI performance?
TIA records which tests touch which source files during the first run. On subsequent runs, Pest executes only the tests affected by changed files and replays cached results for unchanged tests. Large suites that took minutes can complete in seconds. The tradeoff: TIA requires a coverage driver (PCOV or Xdebug) to build the dependency map, and the first run takes longer than a normal run.
How does mutation testing improve test quality beyond code coverage?
Code coverage measures execution paths. A test can achieve 100% coverage by calling every method without asserting anything meaningful. Mutation testing modifies source code (changing operators, removing returns, flipping booleans) and verifies that at least one test fails. Surviving mutations reveal assertions that are too loose or missing entirely. Running php artisan test --mutate produces a mutation score percentage alongside standard coverage.
For more Laravel interview questions, the SharpSkill question bank covers authentication, service container patterns, Eloquent relationships and queue architecture.
Testing HTTP Responses and JSON Assertions
Laravel's HTTP testing helpers verify status codes, headers, JSON structures and redirect targets. Combined with Pest, they form concise integration tests.
use App\Models\User;
describe('API Authentication', function () {
it('rejects unauthenticated requests with 401', function () {
$this->getJson('/api/profile')
->assertUnauthorized();
});
it('returns the authenticated user profile', function () {
$user = User::factory()->create([
'name' => 'John Doe',
'email' => 'john@example.com',
]);
$this->actingAs($user)
->getJson('/api/profile')
->assertOk()
->assertJson([
'data' => [
'name' => 'John Doe',
'email' => 'john@example.com',
],
]);
});
it('validates required fields on registration', function () {
$this->postJson('/api/register', [])
->assertUnprocessable()
->assertJsonValidationErrors(['name', 'email', 'password']);
});
});The describe block groups authentication-related tests. Each test name describes the expected behavior, not the implementation. The assertJsonValidationErrors method checks that specific fields have validation error messages.
Sources
- Pest 5 Released by Laravel News, announcing TIA engine, Agent plugin, and Evals
- Pest 5 Now Available, official documentation with requirements and new features
- What We Know About Laravel 13, covering PHP Attributes, AI SDK, and March 2026 release
- PHPUnit 13 Release Announcement, detailing new assertions and deprecations
What to Remember About Laravel Testing with Pest 5
- Pest 5 with Laravel 13 eliminates boilerplate through a fluent configuration API,
expect()chains and automatic test discovery - The TIA engine reruns only affected tests, cutting large suite execution from minutes to seconds without sacrificing coverage accuracy
- Feature tests should form the core of a Laravel test suite, with unit tests reserved for isolated business logic
- Facade fakes (
Mail::fake(),Queue::fake()) handle framework-level mocking, while Mockery handles third-party dependencies injected via the container - Architecture tests enforce structural rules (no Eloquent in controllers, no debug functions) without runtime overhead
- Mutation testing with
--mutatecatches weak assertions that code coverage alone misses - Factory states and chained
expect()->and()assertions keep test data setup readable and assertions specific - For Laravel interview preparation, understanding mock/fake/spy,
RefreshDatabasebehavior, TIA, and mutation testing demonstrates testing maturity beyond basic coverage
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Can you spot the bug in Laravel?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on September 2, 2026
Tags
Share
Related articles

PHP Laravel Developer Interview Questions 2026: Complete Preparation Guide
Master Laravel interview questions for 2026. From Eloquent ORM and Service Container to queues, testing, and Laravel 13 features, prepare for technical interviews with real-world examples.

Laravel Solutions: Advanced Patterns, Debugging and Interview Questions 2026
Master Laravel solutions with advanced architectural patterns, debugging techniques, and interview-ready knowledge. Covers service classes, repository pattern, debugging with Telescope, and common interview questions for Laravel developers.

Laravel 12 in 2026: New Features, Starter Kits and Interview Questions
Laravel 12 brings redesigned starter kits with React 19, Vue 3, Livewire 4, and WorkOS AuthKit. A complete guide covering new features, upgrade path, and key interview questions for 2026.