Advanced Testing: Mocking & Faking
Advanced Testing: Mocking & Faking
Writing effective tests often requires isolating code from external dependencies like databases, APIs, email services, and file systems. Laravel provides powerful mocking and faking capabilities that allow you to test your application logic independently of these external systems. This lesson explores mocking dependencies with Mockery, using Laravel's built-in fakes for common services, creating partial mocks, using spies, and understanding test doubles.
Understanding Test Doubles
Test doubles are objects that replace real dependencies in tests. Different types serve different purposes in testing strategy.
<?php
namespace Tests\Feature;
use Tests\TestCase;
/**
* Test Doubles Hierarchy:
*
* 1. Dummy - Objects passed around but never actually used (fulfill parameter lists)
* 2. Stub - Provides canned answers to calls made during tests
* 3. Spy - Records information about how it was called
* 4. Mock - Pre-programmed with expectations about calls it will receive
* 5. Fake - Working implementation, but unsuitable for production (in-memory database)
*/
class TestDoublesExamplesTest extends TestCase
{
// DUMMY - Never used, just fills parameter
public function test_dummy_example()
{
$dummy = $this->createMock(\App\Services\Logger::class);
// Logger is passed but never called
$calculator = new \App\Services\Calculator($dummy);
$result = $calculator->add(2, 3);
$this->assertEquals(5, $result);
}
// STUB - Returns predetermined values
public function test_stub_example()
{
$stub = $this->createMock(\App\Services\ExternalApiClient::class);
// Configure stub to return specific data
$stub->method('fetchUserData')
->willReturn([
'id' => 1,
'name' => 'John Doe',
'email' => 'john@example.com'
]);
$service = new \App\Services\UserService($stub);
$user = $service->getUserFromExternalApi(1);
$this->assertEquals('John Doe', $user['name']);
}
// SPY - Records method calls for later verification
public function test_spy_example()
{
$spy = $this->createMock(\App\Services\Logger::class);
// Spy records calls
$spy->expects($this->once())
->method('log')
->with('User registered', $this->anything());
$service = new \App\Services\UserService($spy);
$service->registerUser(['name' => 'Jane', 'email' => 'jane@example.com']);
// Spy verifies the call was made
}
// MOCK - Pre-programmed expectations (strict behavior verification)
public function test_mock_example()
{
$mock = $this->createMock(\App\Services\PaymentGateway::class);
// Mock expects specific calls in specific order
$mock->expects($this->once())
->method('charge')
->with(100.00, 'USD')
->willReturn(['success' => true, 'transaction_id' => 'txn_123']);
$service = new \App\Services\OrderService($mock);
$result = $service->processPayment(100.00, 'USD');
$this->assertTrue($result['success']);
}
// FAKE - Working implementation (Laravel's fakes)
public function test_fake_example()
{
\Illuminate\Support\Facades\Mail::fake();
$service = new \App\Services\NotificationService();
$service->sendWelcomeEmail('user@example.com');
// Fake captured the mail
\Illuminate\Support\Facades\Mail::assertSent(\App\Mail\WelcomeEmail::class);
}
}
Mocking Dependencies with Mockery
Laravel uses Mockery for creating mock objects. Mockery provides expressive syntax for defining expectations and verifying interactions.
<?php
namespace Tests\Unit;
use App\Services\PaymentGateway;
use App\Services\OrderService;
use Mockery;
use Tests\TestCase;
class OrderServiceTest extends TestCase
{
// Basic mock with return value
public function test_creates_order_with_successful_payment()
{
$paymentGateway = Mockery::mock(PaymentGateway::class);
$paymentGateway->shouldReceive('charge')
->once()
->with(100.00, 'USD', Mockery::any())
->andReturn([
'success' => true,
'transaction_id' => 'txn_123'
]);
$service = new OrderService($paymentGateway);
$result = $service->createOrder([
'amount' => 100.00,
'currency' => 'USD',
'items' => [...]
]);
$this->assertTrue($result['success']);
$this->assertEquals('txn_123', $result['transaction_id']);
}
// Mock with multiple method calls
public function test_handles_payment_failure()
{
$paymentGateway = Mockery::mock(PaymentGateway::class);
$paymentGateway->shouldReceive('charge')
->once()
->andReturn(['success' => false, 'error' => 'Insufficient funds']);
$paymentGateway->shouldReceive('logFailure')
->once()
->with(Mockery::type('string'));
$service = new OrderService($paymentGateway);
$result = $service->createOrder(['amount' => 100.00]);
$this->assertFalse($result['success']);
}
// Argument matching
public function test_argument_matching()
{
$gateway = Mockery::mock(PaymentGateway::class);
// Exact value matching
$gateway->shouldReceive('charge')->with(100.00)->once();
// Type matching
$gateway->shouldReceive('log')->with(Mockery::type('string'))->once();
// Pattern matching
$gateway->shouldReceive('notify')
->with(Mockery::pattern('/^user_\d+$/'))
->once();
// Callback matching
$gateway->shouldReceive('validate')
->with(Mockery::on(function ($arg) {
return $arg['amount'] > 0 && $arg['currency'] === 'USD';
}))
->once();
// Array subset matching
$gateway->shouldReceive('process')
->with(Mockery::subset(['amount' => 100, 'currency' => 'USD']))
->once();
}
// Multiple return values
public function test_multiple_return_values()
{
$api = Mockery::mock(\App\Services\ExternalApi::class);
// Return different values on successive calls
$api->shouldReceive('fetchData')
->andReturn(
['status' => 'pending'],
['status' => 'processing'],
['status' => 'complete']
);
$this->assertEquals('pending', $api->fetchData()['status']);
$this->assertEquals('processing', $api->fetchData()['status']);
$this->assertEquals('complete', $api->fetchData()['status']);
}
// Throwing exceptions
public function test_handles_api_exception()
{
$api = Mockery::mock(\App\Services\ExternalApi::class);
$api->shouldReceive('fetchData')
->andThrow(new \Exception('API unavailable'));
$service = new \App\Services\DataService($api);
$this->expectException(\Exception::class);
$service->getData();
}
// Ordered expectations
public function test_ordered_method_calls()
{
$logger = Mockery::mock(\App\Services\Logger::class);
$logger->shouldReceive('info')->with('Starting process')->once()->ordered();
$logger->shouldReceive('debug')->with(Mockery::any())->once()->ordered();
$logger->shouldReceive('info')->with('Process complete')->once()->ordered();
$service = new \App\Services\ProcessService($logger);
$service->runProcess();
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
}
Laravel's Built-in Fakes
Laravel provides fake implementations of common services, allowing you to test code that interacts with these services without actually performing the operations.
<?php
namespace Tests\Feature;
use App\Jobs\ProcessOrder;
use App\Mail\OrderConfirmation;
use App\Models\User;
use App\Notifications\OrderShipped;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class FakesExampleTest extends TestCase
{
// Mail Fake
public function test_sends_order_confirmation_email()
{
Mail::fake();
$user = User::factory()->create();
$order = $this->createOrder($user);
// Assert mail was sent
Mail::assertSent(OrderConfirmation::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
// Assert count
Mail::assertSent(OrderConfirmation::class, 1);
// Assert mail was not sent
Mail::assertNotSent(\App\Mail\InvoiceEmail::class);
// Assert nothing was sent
Mail::assertNothingSent();
}
// Queue Fake
public function test_dispatches_order_processing_job()
{
Queue::fake();
$order = $this->createOrder();
Queue::assertPushed(ProcessOrder::class, function ($job) use ($order) {
return $job->order->id === $order->id;
});
Queue::assertPushed(ProcessOrder::class, 1);
Queue::assertPushedOn('high-priority', ProcessOrder::class);
Queue::assertNotPushed(\App\Jobs\SendNewsletter::class);
}
// Storage Fake
public function test_uploads_product_image()
{
Storage::fake('public');
$file = UploadedFile::fake()->image('product.jpg', 600, 400);
$response = $this->post('/products', [
'name' => 'Test Product',
'image' => $file,
]);
// Assert file was stored
Storage::disk('public')->assertExists('products/' . $file->hashName());
// Assert file contents
$this->assertEquals(
file_get_contents($file->path()),
Storage::disk('public')->get('products/' . $file->hashName())
);
// Assert file doesn't exist
Storage::disk('public')->assertMissing('products/nonexistent.jpg');
}
// Event Fake
public function test_dispatches_order_created_event()
{
Event::fake([\App\Events\OrderCreated::class]);
$order = $this->createOrder();
Event::assertDispatched(\App\Events\OrderCreated::class, function ($event) use ($order) {
return $event->order->id === $order->id;
});
Event::assertDispatched(\App\Events\OrderCreated::class, 1);
Event::assertNotDispatched(\App\Events\OrderCancelled::class);
}
// Notification Fake
public function test_sends_order_shipped_notification()
{
Notification::fake();
$user = User::factory()->create();
$order = $this->createOrder($user);
$order->markAsShipped();
Notification::assertSentTo(
$user,
OrderShipped::class,
function ($notification) use ($order) {
return $notification->order->id === $order->id;
}
);
Notification::assertSentTo($user, OrderShipped::class, 1);
Notification::assertNotSentTo($user, \App\Notifications\PaymentFailed::class);
}
// HTTP Fake
public function test_fetches_external_api_data()
{
Http::fake([
'api.example.com/users/*' => Http::response([
'id' => 1,
'name' => 'John Doe'
], 200),
'api.example.com/posts/*' => Http::response([
'error' => 'Not found'
], 404),
'*' => Http::response(['default' => 'response'], 200),
]);
$service = app(\App\Services\ExternalApiService::class);
$user = $service->fetchUser(1);
$this->assertEquals('John Doe', $user['name']);
// Assert requests were made
Http::assertSent(function ($request) {
return $request->url() === 'https://api.example.com/users/1' &&
$request->method() === 'GET';
});
Http::assertSentCount(1);
}
// Bus Fake (Jobs & Commands)
public function test_dispatches_command_chain()
{
Bus::fake();
$this->artisan('process:orders');
Bus::assertDispatched(ProcessOrder::class);
Bus::assertChained([
\App\Jobs\SendOrderConfirmation::class,
\App\Jobs\UpdateInventory::class,
\App\Jobs\NotifyWarehouse::class,
]);
}
// Cache Fake
public function test_caches_expensive_calculation()
{
\Illuminate\Support\Facades\Cache::fake();
$service = app(\App\Services\CalculationService::class);
$result = $service->calculateWithCache('key', fn() => 42);
\Illuminate\Support\Facades\Cache::assertHas('calculation:key');
$this->assertEquals(42, $result);
}
}
Partial Mocks
Partial mocks allow you to mock only specific methods of a class while keeping the real implementation for others. This is useful when testing classes with complex dependencies.
<?php
namespace Tests\Unit;
use App\Services\OrderService;
use Tests\TestCase;
class PartialMockTest extends TestCase
{
// Partial mock - mock only specific methods
public function test_partial_mock_with_mockery()
{
$service = Mockery::mock(OrderService::class)->makePartial();
// Mock only the external API call
$service->shouldReceive('fetchShippingRates')
->once()
->andReturn([
'standard' => 5.00,
'express' => 15.00,
]);
// All other methods use real implementation
$order = $service->createOrder([
'items' => [['id' => 1, 'quantity' => 2]],
'shipping_method' => 'standard',
]);
$this->assertEquals(5.00, $order->shipping_cost);
}
// Partial mock with Laravel's spy
public function test_partial_mock_with_spy()
{
$service = $this->partialMock(OrderService::class, function ($mock) {
$mock->shouldReceive('sendConfirmationEmail')
->once()
->andReturn(true);
});
// Real implementation is called
$order = $service->createOrder(['items' => [...]]);
// Verify mocked method was called
$this->assertTrue($order->exists);
}
// Mock protected/private methods (for testing)
public function test_mock_protected_method()
{
$service = Mockery::mock(OrderService::class)->makePartial();
// Use shouldAllowMockingProtectedMethods() for protected methods
$service->shouldAllowMockingProtectedMethods();
$service->shouldReceive('calculateTax')
->once()
->andReturn(10.00);
$total = $service->calculateTotal(['subtotal' => 100.00]);
$this->assertEquals(110.00, $total);
}
// Spy - like partial mock but verifies after execution
public function test_spy_example()
{
$service = $this->spy(OrderService::class);
// Execute real implementation
$order = $service->createOrder([
'user_id' => 1,
'items' => [['id' => 1, 'quantity' => 2]],
]);
// Verify methods were called
$service->shouldHaveReceived('validateItems')->once();
$service->shouldHaveReceived('calculateTotal')->once();
$service->shouldHaveReceived('saveOrder')->once();
}
}
Advanced Mocking Patterns
Complex testing scenarios often require sophisticated mocking techniques including method chaining, fluent interfaces, and container binding.
<?php
namespace Tests\Unit;
use Tests\TestCase;
use Mockery;
class AdvancedMockingTest extends TestCase
{
// Mocking method chaining / fluent interfaces
public function test_mock_fluent_interface()
{
$query = Mockery::mock(\Illuminate\Database\Query\Builder::class);
$query->shouldReceive('where')
->with('status', 'active')
->once()
->andReturnSelf();
$query->shouldReceive('orderBy')
->with('created_at', 'desc')
->once()
->andReturnSelf();
$query->shouldReceive('limit')
->with(10)
->once()
->andReturnSelf();
$query->shouldReceive('get')
->once()
->andReturn(collect([
['id' => 1, 'name' => 'Test']
]));
$result = $query->where('status', 'active')
->orderBy('created_at', 'desc')
->limit(10)
->get();
$this->assertCount(1, $result);
}
// Binding mocks to the container
public function test_bind_mock_to_container()
{
$mock = Mockery::mock(\App\Services\PaymentGateway::class);
$mock->shouldReceive('charge')
->once()
->andReturn(['success' => true]);
// Bind mock to container
$this->app->instance(\App\Services\PaymentGateway::class, $mock);
// Controller or service will receive the mock
$response = $this->post('/orders', [
'amount' => 100,
'items' => [...]
]);
$response->assertStatus(201);
}
// Mock with constructor arguments
public function test_mock_with_constructor()
{
$config = ['api_key' => 'test_key', 'timeout' => 30];
$service = Mockery::mock(
\App\Services\ExternalApiClient::class,
[$config]
)->makePartial();
$service->shouldReceive('makeRequest')
->once()
->andReturn(['data' => 'response']);
$result = $service->fetchData();
$this->assertEquals('response', $result['data']);
}
// Mocking static methods
public function test_mock_static_method()
{
// Create alias mock for static methods
$mock = Mockery::mock('alias:' . \App\Services\HelperService::class);
$mock->shouldReceive('generateToken')
->once()
->andReturn('test_token_123');
// Static call will use mock
$token = \App\Services\HelperService::generateToken();
$this->assertEquals('test_token_123', $token);
}
// Complex argument matching with callbacks
public function test_complex_argument_matching()
{
$repo = Mockery::mock(\App\Repositories\UserRepository::class);
$repo->shouldReceive('create')
->once()
->with(Mockery::on(function ($data) {
// Complex validation logic
return isset($data['email']) &&
filter_var($data['email'], FILTER_VALIDATE_EMAIL) &&
isset($data['password']) &&
strlen($data['password']) >= 8 &&
isset($data['terms_accepted']) &&
$data['terms_accepted'] === true;
}))
->andReturn(new \App\Models\User([
'id' => 1,
'email' => 'test@example.com'
]));
$service = new \App\Services\RegistrationService($repo);
$user = $service->register([
'email' => 'test@example.com',
'password' => 'password123',
'terms_accepted' => true,
]);
$this->assertEquals(1, $user->id);
}
}
Exercise 1: Mock External Payment Gateway
Test an order processing service that uses an external payment gateway:
- Create a PaymentGateway interface with methods: charge(), refund(), checkStatus()
- Write tests that mock the gateway for successful payment, failed payment, and timeout scenarios
- Mock the gateway to throw exceptions for network errors
- Verify that the order service handles all scenarios correctly
- Use argument matching to verify correct amounts and currencies are passed
Exercise 2: Test with Laravel Fakes
Test a user registration flow using Laravel's fakes:
- Use Mail::fake() to verify welcome email is sent
- Use Queue::fake() to verify email verification job is dispatched
- Use Event::fake() to verify UserRegistered event is fired
- Use Storage::fake() to test profile picture upload
- Verify all fakes with proper assertions (assertSent, assertPushed, assertDispatched)
- Test that welcome email contains user's name
Exercise 3: Partial Mocks and Spies
Test a report generation service using partial mocks:
- Create ReportService with methods: generateReport(), fetchData(), formatData(), saveToFile()
- Use partial mock to mock only fetchData() method (simulate slow external API)
- Let formatData() and saveToFile() use real implementation
- Use a spy to verify formatData() was called with correct data structure
- Verify saveToFile() was called with properly formatted data
- Test that the full report generation workflow works correctly
Summary
In this lesson, you've mastered advanced testing techniques for isolating code from external dependencies. You learned the different types of test doubles (dummies, stubs, spies, mocks, fakes) and when to use each, explored Mockery for creating sophisticated mocks with expectations and argument matching, used Laravel's built-in fakes for Mail, Queue, Storage, Events, Notifications, and HTTP, implemented partial mocks and spies for testing specific methods while preserving real implementations, and applied advanced mocking patterns for fluent interfaces, container binding, and complex scenarios. These techniques enable you to write fast, reliable tests that verify your application logic independently of external systems.