0

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"