Stub — a test double that returns hardcoded values, no expectations
Beginner5 min read·eng-17-006
interview
Concept
Stub — a test double that returns predefined (hardcoded) values when called. Unlike a mock, a stub does NOT verify that it was called. It just provides controlled return values.
The difference from a mock:
- Stub: "When called, return this value." No verification.
- Mock: "Must be called exactly once with these args. Also return this value."
When to use stubs:
- You need a dependency to return a specific value so you can test your code's reaction to it.
- You don't care if the dependency was called — only what happens in your code after.
- You want to test error paths (stub throws an exception).
Stub for different scenarios:
willReturn($value): Always returns this value.willReturnMap([]): Returns different values for different arguments.willThrowException(new \Exception('error')): Throws on call.willReturnCallback(fn() => ...): Custom logic.
Common stubs in Laravel:
Http::fake(): Stubs HTTP responses so no real network request happens.Cache::fake(): Stubs the cache driver.- Stubbing Eloquent models in unit tests.
Stubbing in PHPUnit: You create a mock object (same API) but use method()->willReturn() without expects(). Without expects(), it's a stub — no verification.
Code Example
php
<?php
// STUB — returns controlled values, no verification
class OrderServiceTest extends \PHPUnit\Framework\TestCase
{
public function test_creates_order_when_payment_succeeds(): void
{
// STUB: PaymentGateway always returns success
$stubGateway = $this->createStub(PaymentGateway::class); // createStub = mock without expectations
$stubGateway->method('charge')
->willReturn(new PaymentResult(success: true, transactionId: 'tx_abc123'));
// Don't care if charge() is called once, twice, or never
// Just need it to return success when called
$service = new OrderService($stubGateway, new InMemoryOrderRepository());
$order = $service->place(['total' => 100], 'tok_valid');
$this->assertEquals('paid', $order->status);
$this->assertEquals('tx_abc123', $order->transactionId);
}
public function test_marks_order_failed_when_payment_declines(): void
{
// STUB: PaymentGateway returns failure
$stubGateway = $this->createStub(PaymentGateway::class);
$stubGateway->method('charge')
->willReturn(new PaymentResult(success: false, transactionId: null, error: 'Card declined'));
$service = new OrderService($stubGateway, new InMemoryOrderRepository());
$order = $service->place(['total' => 100], 'tok_decline');
$this->assertEquals('payment_failed', $order->status);
}
public function test_handles_gateway_exception(): void
{
// STUB: throws exception
$stubGateway = $this->createStub(PaymentGateway::class);
$stubGateway->method('charge')
->willThrowException(new \RuntimeException('Connection timeout'));
$service = new OrderService($stubGateway, new InMemoryOrderRepository());
$this->expectException(\App\Exceptions\PaymentException::class);
$service->place(['total' => 100], 'tok_timeout');
}
public function test_returns_different_values_for_different_args(): void
{
// STUB with argument-based responses
$stubRepo = $this->createStub(OrderRepositoryInterface::class);
$stubRepo->method('find')
->willReturnMap([
[1, new Order(['id' => 1, 'status' => 'paid'])],
[2, new Order(['id' => 2, 'status' => 'pending'])],
[99, null], // not found
]);
$this->assertEquals('paid', $stubRepo->find(1)->status);
$this->assertEquals('pending', $stubRepo->find(2)->status);
$this->assertNull($stubRepo->find(99));
}
}
// Laravel Http::fake() — stub for HTTP calls
\Http::fake([
'https://api.stripe.com/*' => \Http::response(['id' => 'ch_123', 'status' => 'succeeded'], 200),
'https://api.example.com/*' => \Http::response(['error' => 'Not found'], 404),
'*' => \Http::response('', 500), // all others fail
]);
// Now any Http::post('https://api.stripe.com/charges') returns the stubbed response
$result = Http::post('https://api.stripe.com/v1/charges', [...]);
$result->status(); // 200
$result->json(); // ['id' => 'ch_123', 'status' => 'succeeded']