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

Laravel testing best practices have shifted significantly with Pest 4, mutation testing and architecture presets. Laravel 12 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 4.7 (built on PHPUnit 12) is the default testing framework for Laravel 12. It requires PHP 8.3+ and introduces mutation testing, time-balanced sharding, and architecture presets out of the box.
Setting Up Pest 4 in a Laravel 12 Project
Every new Laravel 12 application scaffolds Pest automatically. For existing projects, the migration from PHPUnit or Pest 3 takes minutes. 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.
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 4 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 4 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 4 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 4 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. Combine with Pest 4's time-balanced sharding for optimal CI performance.
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 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.
Conclusion
- Pest 4 with Laravel 12 eliminates boilerplate through a fluent configuration API,
expect()chains and automatic test discovery - 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 the mock/fake/spy distinction,
RefreshDatabasebehavior and mutation testing demonstrates testing maturity beyond basic coverage
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

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.

Laravel and PHP Interview Questions: Top 25 in 2026
The 25 most common Laravel and PHP interview questions. Eloquent ORM, middleware, artisan, queues, tests and architecture with detailed answers and code examples.

Laravel Middleware Deep Dive: Authentication, Rate Limiting and Custom Middleware
Master Laravel middleware with practical examples covering authentication guards, rate limiting with throttle, custom middleware creation, and advanced patterns for production applications.