Skip to main content
Create your own
Lesson illustration

Mocking External Services in Tests

Hello! Welcome to the final lesson in our module on Production-Ready Testing.

In our last session, we focused on testing code that interacts directly with the database. You learned how to use the RefreshDatabase trait and model factories to create a clean, consistent, and fast testing environment. This allows you to confidently verify that your application correctly manipulates database records.

However, modern applications rarely interact with just a database. They send emails, dispatch background jobs, and communicate with third-party APIs. Running tests that trigger these real-world actions is slow, unreliable, and can have unintended consequences (like sending hundreds of test emails).

Today, we'll solve this problem. Our learning outcome is to use mocks and fakes to simulate dependencies and external services in tests. You'll learn how to isolate your application code from the outside world, allowing you to test your logic without making real network requests or sending actual notifications.


1. Why Simulate Dependencies? The Problem with "Real" Services

When a unit or feature test makes a real network call to an external service, it introduces several problems:

  • Slowness: Network latency can make your test suite take minutes to run instead of seconds.
  • Unreliability: The external API might be down, or your network connection could fail, causing your tests to fail for reasons unrelated to your code's correctness.
  • Side Effects: You don't want to actually charge a credit card, send a real SMS, or create a user in a live CRM every time you run your tests.

Testing with real services makes your tests "brittle" and slow. The solution is to replace these real dependencies with "test doubles"—objects that look and act like the real thing but are completely under your control within the test.

To see a practical demonstration of this problem, let's watch a short segment of a video that introduces the concept of mocking.

PHPUnit Tutorial Part 2 - Mocking - Full PHP 8 Tutorial

This video from the Program With Gio channel perfectly illustrates why running tests with real, un-mocked dependencies leads to slow and unpredictable (flaky) test results.

Watch from 00:14 to 05:55. Pay close attention to: The reasons given for why mocking is useful (APIs, email services, etc.). How the test without mocks is both slow (due to sleep() functions simulating latency) and unreliable (due to random return values). This perfectly captures the problem we're trying to solve.


2. Mocks vs. Fakes: A Quick Primer

In testing, you'll hear the terms "mock" and "fake" used frequently, sometimes interchangeably. While they are both types of "test doubles," there's a helpful distinction:

  • Mock: A dummy object where you define expectations upfront. You use a mock to verify interactions. For example: "I expect the send method on my email service to be called exactly once with this specific user's email address."
  • Fake: A lightweight, working implementation of a service, but designed for testing. You use a fake to verify outcomes. For example, Laravel's NotificationFake doesn't send real notifications; instead, it adds them to an internal array that you can inspect.

Laravel provides a powerful mocking library called Mockery under the hood, but it also gives us high-level "fakes" for its most common systems, which makes testing much simpler.


3. Mocking Objects and Dependencies

To effectively mock a dependency, your code must follow the principle of Dependency Injection. The video you just watched demonstrated this well: instead of creating a dependency inside a method with new PaymentGatewayService(), you "inject" it through the class's constructor. This allows you to replace the real service with a mock during testing.

Creating Mocks in Laravel

While PHPUnit provides createMock, Laravel offers even more convenient helpers that automatically handle binding the mock into the service container.

Mocking Objects

The official Laravel documentation explains its convenient wrapper methods for creating mocks and spies.

Read the section titled 'Mocking Objects'. Focus on the code examples for mock, partialMock, and spy to understand what they do. You'll see that $this->mock(...) is much cleaner than the manual Mockery::mock(...) and $this->instance(...) combination.

Let's break down the key concepts from the documentation and the video:

  1. Creating a Mock: $mock = $this->mock(MyService::class);
    This creates a mock of MyService and tells Laravel's service container to use this mock whenever MyService is requested.

  2. Stubbing a Method: $mock->shouldReceive('process')->andReturn(true);
    This is called "stubbing". You are telling the mock: "When the process method is called, don't run the real code; just return true."

  3. Asserting an Interaction (Spying): $spy->shouldHaveReceived('process');
    A "spy" is a special kind of mock that records method calls. This allows you to execute your code and then assert after the fact that a certain method was called.

Let's imagine you have a service that processes an order and relies on an external ShippingService.

// app/Services/OrderProcessingService.php
class OrderProcessingService
{
    public function __construct(private ShippingService $shipping) {}

    public function execute(Order $order)
    {
        // ... some logic
        $trackingNumber = $this->shipping->dispatch($order->id, $order->address);
        $order->update(['tracking_number' => $trackingNumber]);
        // ... more logic
    }
}

// tests/Feature/OrderProcessingTest.php
use App\Services\ShippingService;
use App\Services\OrderProcessingService;

test('it assigns a tracking number when processing an order', function () {
    // 1. Arrange
    $order = Order::factory()->create();
    
    // Create a mock of the ShippingService
    $shippingMock = $this->mock(ShippingService::class);

    // Stub the 'dispatch' method to return a fake tracking number
    $shippingMock->shouldReceive('dispatch')
                 ->once() // Expect it to be called one time
                 ->with($order->id, $order->address) // With these specific arguments
                 ->andReturn('ABC-123-XYZ'); // And return this value

    // 2. Act
    // Resolve the service from the container; Laravel will inject our mock!
    $processor = app(OrderProcessingService::class);
    $processor->execute($order);

    // 3. Assert
    $this->assertDatabaseHas('orders', [
        'id' => $order->id,
        'tracking_number' => 'ABC-123-XYZ',
    ]);
});

This test verifies our OrderProcessingService logic without ever touching the real ShippingService.


4. Using Laravel's Built-in Fakes

For many of its core features, Laravel provides dedicated "fake" implementations that are incredibly easy to use. These are high-level abstractions over the mocking concepts we just discussed.

Faking HTTP Requests

One of the most common needs is to fake calls to external APIs. Laravel's Http facade has a powerful fake() method for this.

Let's watch a video that provides a clear, practical walkthrough.

Advanced Laravel Testing: 3rd-Party APIs with HTTP Fake

The Laravel Daily channel gives a great demonstration of using Http::fake() to test code that interacts with a third-party API.

Watch from 00:55 to 04:12. Focus on: The concept of 'fixtures' (pre-prepared JSON responses). The core syntax: Http::fake([...]) where you map a URL pattern to a response. How the application code uses the standard Http::get(...) but the test intercepts it.

As the video shows, the pattern is straightforward:

  1. Call Http::fake(): In your test, you tell the HTTP client to start faking requests. You can provide an array mapping URL patterns to responses.
  2. Run your code: Your application code makes its request as usual using Http::get(), Http::post(), etc.
  3. Assert: Use Http::assertSent() to verify that a request was made to the correct URL.

Here's an example testing a controller that fetches weather data:

// tests/Feature/WeatherTest.php
use Illuminate\Support\Facades\Http;

test('it displays the weather for a given city', function () {
    // Tell the HTTP client to fake requests to the weather API
    Http::fake([
        'api.weather.com/*' => Http::response([
            'temperature' => '15',
            'condition' => 'Sunny',
        ], 200)
    ]);

    // Act: Make a request to our application's endpoint
    $response = $this->get('/api/weather/london');
    
    // Assert: Check the response from our application
    $response->assertOk();
    $response->assertJson([
        'temp_celsius' => 15,
        'status' => 'Sunny',
    ]);

    // Assert that a request was sent to the correct external URL
    Http::assertSent(function ($request) {
        return $request->url() == 'https://api.weather.com/forecast?city=london';
    });
});

Faking Other Systems (Notifications, Jobs, Mail, Events)

This same ::fake() pattern applies to many other Laravel facades. This allows you to test that your application dispatched an action without that action actually running.

Laravel SSH Facade Faking Example in a Test
This image shows a perfect, concise example of the fake/assert pattern. `SSH::fake()` prevents a real SSH command from running, and `SSH::assertExecuted()` verifies that the application tried to run it.

Here are some of the most common fakes you will use:

Complete Laravel Testing Guide

This testing guide provides several excellent, copy-pasteable examples of using Laravel's other built-in fakes for notifications, jobs, and more.

This is a great resource to bookmark. For now, quickly read the subsections titled 'Notification Fake' and 'Testing Individual Jobs' (within the 'Testing Jobs, Queues, and Batches' section). Notice the consistent ::fake() and ::assert...() pattern.

Let's summarize the pattern for a few key systems:

  • Notifications:

    use Illuminate\Support\Facades\Notification;
    
    Notification::fake();
    // ... your code that sends a notification ...
    Notification::assertSentTo($user, WelcomeNotification::class);
    
  • Queued Jobs:

    use Illuminate\Support\Facades\Bus;
    
    Bus::fake();
    // ... your code that dispatches a job ...
    Bus::assertDispatched(ProcessLargeImport::class);
    
  • Mail:

    use Illuminate\Support\Facades\Mail;
    
    Mail::fake();
    // ... your code that sends an email ...
    Mail::assertSent(OrderShippedMailable::class, function ($mail) use ($order) {
        return $mail->hasTo($order->user->email);
    });
    

In all these cases, you are testing your application's logic in isolation. You confirm that your code correctly triggered an action, and you can test the action itself (the notification content, the job's handle method) in a separate, dedicated unit test.


Conclusion

You have now learned how to decouple your tests from external services, a critical skill for building a robust and maintainable test suite. By using mocks and fakes, you can write tests that are fast, reliable, and focused purely on your application's logic.

Key Takeaways:

  • Testing with real external services is slow, unreliable, and can have unwanted side effects.
  • Mocks are used to verify interactions (e.g., "was this method called?"). They require dependencies to be injected.
  • Fakes are lightweight test implementations used to verify outcomes (e.g., "was a notification added to the list of sent notifications?").
  • Laravel provides convenient helpers like $this->mock() to easily mock your own services.
  • Laravel's built-in fakes (Http::fake(), Notification::fake(), Bus::fake(), etc.) make it trivial to test code that interacts with core Laravel systems.
  • The common pattern is to call Facade::fake() at the start of your test and Facade::assert...() at the end.

Up Next:

This lesson concludes our module on Production-Ready Testing! You now have a comprehensive toolkit for writing unit, feature, and database tests, along with the skills to handle external dependencies.

With this solid foundation in quality assurance, we'll now move on to Module 6: API Development: Resources & Versioning. In our next lesson, we'll start building professional, production-quality APIs by learning how to control and standardize your JSON responses using Laravel's powerful API Resources.

Can't find a good explanation? Sign up and we'll make it for you

Sign up