# 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. - Published: 2026-06-04 - Updated: 2026-06-04 - Author: SharpSkill - Tags: laravel, testing, pest, php, mocking, best-practices, interview - Reading time: 9 min --- 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 Is the Standard** > > 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. ```php // tests/Pest.php 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. ```php // tests/Feature/UserRegistrationTest.php 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. ```php // tests/Unit/PriceCalculatorTest.php 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](https://github.com/mockery/mockery) handles everything else. ```php // tests/Feature/OrderProcessingTest.php 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. > **Mock at Boundaries** > > 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: ```php // tests/Feature/PaymentGatewayTest.php 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](https://pestphp.com/docs/arch-testing) that enforces structural rules across the codebase. These tests run against the AST, not the runtime, making them extremely fast. ```php // tests/Architecture/ArchitectureTest.php 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: ```php // tests/Architecture/LaravelPresetTest.php 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. ## Mutation Testing to Verify Test Quality Code coverage measures which lines execute during tests. [Mutation testing](https://pestphp.com/docs/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. ```bash # Run mutation testing on a specific class php artisan test --mutate --class=App\\Services\\PriceCalculator ``` Pest 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. ```php // 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. ```php // tests/Feature/ArticlePublishingTest.php 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. > **Parallel Testing** > > 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](/technologies/laravel) 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](/blog/laravel/laravel-php-interview-questions), the SharpSkill question bank covers authentication, [service container patterns](/technologies/laravel/interview-questions/service-container-di), 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. ```php // tests/Feature/ApiAuthenticationTest.php 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 `--mutate` catches 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](/technologies/laravel), understanding the mock/fake/spy distinction, `RefreshDatabase` behavior and mutation testing demonstrates testing maturity beyond basic coverage --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/laravel/laravel-testing-pest-mocking-best-practices