Fake — a test double with a real but simplified implementation
Beginner5 min read·eng-17-008
interview
Concept
Fake — a test double with a real but simplified working implementation. Unlike a stub (which returns hardcoded values) or a mock (which verifies calls), a fake actually works — it just works in a simpler way than the production version.
The fake is functional: A FakePaymentGateway actually processes payments — but in memory, not via Stripe. A FakeCache actually caches — but in an array, not Redis.
Why fakes are useful:
- More realistic than stubs: The fake actually runs logic, so you can test behaviors that depend on state (a cache miss followed by a cache hit).
- No external dependencies: No Redis, no Stripe, no SMTP server needed.
- Can have test helpers:
$fakeCache->assertStored('key'),$fakePaymentGateway->refund()can verify state.
When to use fakes vs mocks:
- Fake: When the dependency has meaningful state or complex behavior that you want to test WITH.
- Mock: When you just need to verify a specific method call happened.
- Stub: When you just need a return value.
Laravel's built-in fakes:
Mail::fake()— records what would be sent, doesn't actually send.Queue::fake()— records what would be queued, doesn't actually run jobs.Event::fake()— records what events were dispatched.Storage::fake()— in-memory filesystem.Http::fake()— stub HTTP responses.Notification::fake()— records sent notifications.
Building a custom fake: Implement the interface with in-memory storage. Add assertion helpers.
Code Example
php
<?php
// FAKE PAYMENT GATEWAY — real logic, no Stripe API
class FakePaymentGateway implements PaymentGateway
{
private array $charges = [];
private array $refunds = [];
public function charge(int $amountCents, string $currency, string $token): PaymentResult
{
// Simulate failure for specific token
if ($token === 'tok_fail') {
return new PaymentResult(success: false, transactionId: null, error: 'Declined');
}
// Actually "processes" the charge in memory
$id = 'fake_ch_' . count($this->charges);
$this->charges[$id] = ['amount' => $amountCents, 'currency' => $currency, 'token' => $token];
return new PaymentResult(success: true, transactionId: $id);
}
public function refund(string $chargeId): void
{
if (!isset($this->charges[$chargeId])) throw new \RuntimeException("Unknown charge: {$chargeId}");
$this->refunds[] = $chargeId;
unset($this->charges[$chargeId]);
}
// Test helper methods — not in the interface, only for tests
public function totalCharged(): int { return array_sum(array_column($this->charges, 'amount')); }
public function chargeCount(): int { return count($this->charges); }
public function wasRefunded(string $id): bool { return in_array($id, $this->refunds); }
}
// Tests using the fake
class CheckoutTest extends \PHPUnit\Framework\TestCase
{
private FakePaymentGateway $gateway;
protected function setUp(): void
{
$this->gateway = new FakePaymentGateway();
}
public function test_charges_correct_amount(): void
{
$service = new CheckoutService($this->gateway);
$service->checkout(new Cart(totalCents: 4999), 'tok_visa');
$this->assertEquals(4999, $this->gateway->totalCharged());
$this->assertEquals(1, $this->gateway->chargeCount());
}
public function test_handles_failed_payment(): void
{
$service = new CheckoutService($this->gateway);
$service->checkout(new Cart(totalCents: 100), 'tok_fail');
$this->assertEquals(0, $this->gateway->chargeCount()); // no charge recorded
}
public function test_refund_after_cancellation(): void
{
$service = new CheckoutService($this->gateway);
$order = $service->checkout(new Cart(totalCents: 999), 'tok_visa');
$chargeId = $order->chargeId;
$service->cancelAndRefund($order);
$this->assertTrue($this->gateway->wasRefunded($chargeId));
}
}
// LARAVEL'S STORAGE FAKE — in-memory filesystem
\Storage::fake('s3');
$service = new AvatarUploadService();
$service->upload($user, UploadedFile::fake()->image('avatar.jpg'));
\Storage::disk('s3')->assertExists("avatars/{$user->id}.jpg");
// No actual S3 connection — fake verified the file was "uploaded"