| name | acc-create-test-double |
| description | Generates test doubles (Mocks, Stubs, Fakes, Spies) for PHP 8.5. Creates appropriate double type based on testing needs with PHPUnit MockBuilder patterns. |
Test Double Generator
Generates appropriate test doubles based on testing needs.
Test Double Types
| Type | Purpose | Behavior | Verification |
|---|
| Stub | Provide canned answers | Returns predefined values | No |
| Mock | Verify interactions | Configurable | Yes (expectations) |
| Fake | Working implementation | Real logic, simplified | No |
| Spy | Record interactions | Passes through | Yes (inspection) |
Decision Matrix
Need to verify method was called?
├── Yes → Need to record actual calls?
│ ├── Yes → Spy
│ └── No → Mock
└── No → Need real behavior?
├── Yes → Fake
└── No → Stub
When to Use Each
| Scenario | Double Type | Example |
|---|
| External API response | Stub | HTTP client returns fixed JSON |
| Verify email sent | Mock | Assert send() was called |
| Repository for domain tests | Fake | InMemory implementation |
| Audit logging | Spy | Record all log calls |
| Time-dependent code | Fake | FrozenClock |
Stub Examples
PHPUnit Stub
use PHPUnit\Framework\TestCase;
final class OrderServiceTest extends TestCase
{
public function test_calculates_order_total(): void
{
$taxCalculator = $this->createStub(TaxCalculatorInterface::class);
$taxCalculator->method('calculate')
->willReturn(Money::EUR(100));
$service = new OrderService($taxCalculator);
$total = $service->calculateTotal($order);
self::assertEquals(Money::EUR(1100), $total);
}
}
Stub with Different Returns
public function test_handles_multiple_calls(): void
{
$repository = $this->createStub(UserRepositoryInterface::class);
$repository->method('findById')
->willReturnCallback(function (UserId $id) {
return match ($id->toString()) {
'user-1' => UserMother::john(),
'user-2' => UserMother::jane(),
default => null,
};
});
$repository->method('findById')
->willReturnOnConsecutiveCalls(
UserMother::john(),
UserMother::jane(),
null
);
}
Manual Stub Class
<?php
declare(strict_types=1);
namespace Tests\Stub;
use App\Infrastructure\Http\HttpClientInterface;
use App\Infrastructure\Http\HttpResponse;
final class FixedHttpClientStub implements HttpClientInterface
{
public function __construct(
private readonly HttpResponse $response
) {}
public function get(string $url, array $headers = []): HttpResponse
{
return $this->response;
}
public function post(string $url, array $data, array $headers = []): HttpResponse
{
return $this->response;
}
{
( (, ()));
}
{
::(, );
}
{
::(, [ => ]);
}
}
Mock Examples
PHPUnit Mock
public function test_sends_notification_on_order_placed(): void
{
$notifier = $this->createMock(NotifierInterface::class);
$notifier->expects($this->once())
->method('notify')
->with(
$this->isInstanceOf(OrderPlacedNotification::class)
);
$service = new OrderService($this->repository, $notifier);
$service->placeOrder($command);
}
Mock with Argument Matching
public function test_logs_with_correct_context(): void
{
$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->once())
->method('info')
->with(
'Order placed',
$this->callback(function (array $context) {
return isset($context['orderId'])
&& isset($context['customerId'])
&& $context['amount'] > 0;
})
);
$service = new OrderService($this->repository, $logger);
$service->placeOrder($command);
}
Mock Method Call Count
public function test_retries_on_failure(): void
{
$client = $this->createMock(HttpClientInterface::class);
$client->expects($this->exactly(3))
->method('post')
->willThrowException(new ConnectionException());
$service = new PaymentService($client, maxRetries: 3);
$this->expectException(PaymentFailedException::class);
$service->charge($payment);
}
Fake Examples
InMemory Repository (Fake)
<?php
declare(strict_types=1);
namespace Tests\Fake;
use App\Domain\User\User;
use App\Domain\User\UserId;
use App\Domain\User\UserRepositoryInterface;
final class InMemoryUserRepository implements UserRepositoryInterface
{
private array $users = [];
public function save(User $user): void
{
$this->users[$user->id()->toString()] = $user;
}
public function findById(UserId $id): ?User
{
return $this->users[$id->toString()] ?? ;
}
{
(->users[->()->()]);
}
{
->users = [];
}
{
(->users);
}
}
Frozen Clock (Fake)
<?php
declare(strict_types=1);
namespace Tests\Fake;
use Psr\Clock\ClockInterface;
use DateTimeImmutable;
final class FrozenClock implements ClockInterface
{
public function __construct(
private DateTimeImmutable $now
) {}
public function now(): DateTimeImmutable
{
return $this->now;
}
public static function at(string $datetime): self
{
return new self(new DateTimeImmutable($datetime));
}
public function advance(string $interval): void
{
$this->now = ->now->();
}
}
= ::();
= ();
= ->();
::(, ->()->());
->();
::(->(->()));
Spy Examples
Collecting Spy
<?php
declare(strict_types=1);
namespace Tests\Spy;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
final class SpyLogger implements LoggerInterface
{
private array $logs = [];
public function log($level, string|\Stringable $message, array $context = []): void
{
$this->logs[] = [
'level' => $level,
'message' => (string) $message,
'context' => $context,
];
}
public function emergency(string|\Stringable $message, array $context = []):
{
->(::, , );
}
{
->(::, , );
}
{
->(::, , );
}
{
->(::, , );
}
{
->(::, , );
}
{
->(::, , );
}
{
->(::, , );
}
{
->(::, , );
}
{
->logs;
}
{
(->logs ) {
([] === && ([], )) {
;
}
}
;
}
{
->logs = [];
}
}
Usage
public function test_logs_order_placement(): void
{
$logger = new SpyLogger();
$service = new OrderService($this->repository, $logger);
$service->placeOrder($command);
self::assertTrue($logger->hasLogged('info', 'Order placed'));
$logs = $logger->getLogs();
self::assertCount(1, $logs);
self::assertArrayHasKey('orderId', $logs[0]['context']);
}
Generation Instructions
-
Analyze the dependency:
- Interface or class being doubled
- Methods that need doubling
- Expected behavior in test
-
Choose double type:
- Verification needed? → Mock or Spy
- Real behavior needed? → Fake
- Just return values? → Stub
-
Generate appropriate implementation:
- PHPUnit built-in for simple cases
- Custom class for complex behavior
-
File placement:
- Stubs:
tests/Stub/
- Fakes:
tests/Fake/
- Spies:
tests/Spy/
- Or combined in
tests/Double/
Best Practices
- Mock interfaces, not classes — avoid mocking final classes
- Prefer Fakes for repositories — more realistic behavior
- Use Stubs for external services — HTTP, payment gateways
- Limit to 3 mocks per test — more indicates design issue
- Never mock Value Objects — use real instances
- Don't mock what you don't own — wrap external libraries first