Fake Implementations
When mocking is not enough
Mocks are great for isolated unit tests. But sometimes you need a fake that behaves like the real thing.
When to use a fake
- - The dependency has complex behavior you want to simulate
- - Multiple tests need the same fake setup
- - You want to assert on state, not just method calls
- - The real service is slow, flaky, or costs money
The Contract
// app/Contracts/PaymentGateway.php
interface PaymentGateway
{
public function charge(int $amount, string $token): array;
}
The Fake Implementation
// replaces real implementation in test e.g. StripePaymentGateway
// app/Services/Payment/FakePaymentGateway.php
class FakePaymentGateway implements PaymentGateway
{
public function charge(int $amount, string $token): array
{
return [
'id' => 'ch_' . uniqid(),
'status' => 'succeeded',
'amount' => $amount,
];
}
}
The Controller Test with Fake
// tests/Feature/CheckoutControllerTest.php
class CheckoutControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->app->bind(PaymentGateway::class, FakePaymentGateway::class);
}
#[Test]
public function order_is_charged_successfully(): void
{
$response = $this->postJson('/checkout', [
'total' => 100,
'payment_token' => 'tok_visa',
]);
$response->assertOk()
->assertJson([
'status' => 'succeeded',
'amount' => 100,
]);
$this->assertDatabaseHas('orders', [
'total' => 100,
'payment_token' => 'tok_visa',
'status' => 'paid',
]);
}
}
Why fakes beat mocks
- - Track state across multiple calls
- - No fragile mock expectations
- - Reusable across many tests
- - Can be tested with contract tests
Make sure your fake behaves like the real thing. Use contract tests — run the same test suite against both the fake and the real implementation.
Read more by @jcergolj: Contract Tests