Back to Blog
Lesson 51 of the Intermediate Laravel: Real-World Application Patterns course
LaravelJune 26, 20263 min read

Testing with Test Doubles: Mastering Mocks and Stubs in Laravel

Stop hitting live APIs in your tests. Learn how to use test doubles, stubs, and mocks to isolate your Laravel application logic for faster, reliable tests.

LaravelTestingPHPUnitMockeryMockingStubsphpbackend

Previously in this course, we explored Advanced Testing: Integration Tests in Laravel, where we covered maintaining database state across complex workflows. While integration tests are essential, they can become slow and fragile when they rely on third-party APIs like Stripe, Twilio, or GitHub.

In this lesson, we move beyond database-centric testing to isolate your business logic entirely using test doubles. By replacing real network-dependent objects with controlled substitutes, you ensure your tests remain deterministic, lightning-fast, and decoupled from the instability of the outside world.

The First Principles of Test Doubles

In testing, a "test double" is a generic term for any object used in place of a real production object. Think of it like a stunt double in a movie: they look and act like the real thing, but they are under your complete control.

We primarily use three types of doubles in Laravel:

  • Stubs: Provide canned answers to calls made during the test. Use these when your code needs data from an external service to proceed.
  • Mocks: Assert that specific methods were called with specific arguments. Use these when you need to verify that your code triggered an action (e.g., "Did I actually send this email?").
  • Fakes: Higher-level abstractions provided by Laravel that handle the heavy lifting for you (e.g., Mail::fake()).

Mocking External Services in Practice

Imagine our project board application needs to fetch a user's GitHub profile to display their avatar. If we hit the GitHub API every time we run our test suite, we risk hitting rate limits or causing intermittent failures due to network latency.

Instead, we mock the service. First, ensure your service is behind an interface, which we touched on in our Repository Pattern Fundamentals lesson.

The Worked Example

Suppose we have a GitHubClientInterface. We want to test that our ProfileService correctly handles the user's data.

PHP
#6A9955">// In your test file
public function test_it_returns_formatted_profile_data()
{
    #6A9955">// 1. Create a mock of the interface
    $mock = Mockery::mock(GitHubClientInterface::class);

    #6A9955">// 2. Define the expectation(The Stub)
    $mock->shouldReceive('getUser')
         ->once()
         ->with('laravel-user')
         ->andReturn(['name' => 'John Doe', 'avatar' => 'https:#6A9955">//path.to/image.jpg']);

    #6A9955">// 3. Bind the mock into the Laravel Container
    $this->app->instance(GitHubClientInterface::class, $mock);

    #6A9955">// 4. Run the code that uses the service
    $service = app(ProfileService::class);
    $result = $service->getProfile('laravel-user');

    #6A9955">// 5. Assert the result
    $this->assertEquals('John Doe', $result['name']);
}

By binding the mock into the container, any class that resolves GitHubClientInterface via dependency injection will receive our "stunt double" instead of the real HTTP client.

Hands-on Exercise

For our running project, implement a test for your TaskAssignmentService.

  1. Identify the external dependency (e.g., a notification service or an external task tracker).
  2. Create a test file using php artisan make:test TaskAssignmentTest.
  3. Use Mockery::mock() to simulate a successful response from that service.
  4. Assert that your service correctly processes the response and updates the Task record in the database.

Common Pitfalls

Even senior engineers fall into these traps when using test doubles:

  • Mocking Implementation Details: Don't mock every single private method. If you find yourself mocking too much, your code is likely too tightly coupled. Test the public interface, not the internal steps.
  • Over-Mocking: If you mock everything, you aren't testing your application; you're testing your ability to configure mocks. Only mock what you cannot control (network, time, random number generators).
  • Forgetting to Reset: If you bind a mock to the container in setUp, ensure you are aware of the scope. Laravel's Mockery integration usually cleans up, but manual instance() bindings might persist if not handled carefully.

Summary

Test doubles are the secret to a stable CI/CD pipeline. By using stubs to provide data and mocks to verify interactions, you decouple your business logic from the unpredictable nature of external infrastructure. This is the foundation for the Mocking Services and Repositories in Laravel Tests pattern, allowing you to build complex features with absolute confidence.

Up next: We'll dive into Testing Events and Jobs, where we'll leverage Laravel's built-in fakes to verify asynchronous workflows without actually firing off background processes.

Similar Posts