Mock Repository Generator
Generates InMemory (Fake) repository implementations for testing.
Characteristics
- No database — stores entities in memory
- Fast — no I/O operations
- Isolated — fresh state per test
- Deterministic — predictable behavior
- Implements interface — same contract as real repository
Template
<?php
declare(strict_types=1);
namespace Tests\Fake;
use {RepositoryInterface};
use {Entity};
use {EntityId};
final class InMemory{Entity}Repository implements {RepositoryInterface}
{
/** @var array<string, {Entity}> */
private array $entities = [];
public function save({Entity} $entity): void
{
$this->entities[$entity->id()->toString()] = $entity;
}
public function findById({EntityId} $id): ?{Entity}
{
return $this->entities[$id->toString()] ?? null;
}
public function delete({Entity} $entity): void
{
unset($this->entities[$entity->id()->toString()]);
}
/** @return list<{Entity}> */
public function findAll(): array
{
return array_values($this->entities);
}
public function clear(): void
{
$this->entities = [];
}
}
Complete Examples
User Repository
<?php
declare(strict_types=1);
namespace Tests\Fake;
use App\Domain\User\User;
use App\Domain\User\UserId;
use App\Domain\User\Email;
use App\Domain\User\UserRepositoryInterface;
final class InMemoryUserRepository implements UserRepositoryInterface
{
/** @var array<string, User> */
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()] ?? null;
}
public function findByEmail(Email $email): ?User
{
foreach ($this->users as $user) {
if ($user->email()->equals($email)) {
return $user;
}
}
return null;
}
public function delete(User $user): void
{
unset($this->users[$user->id()->toString()]);
}
public function existsByEmail(Email $email): bool
{
return $this->findByEmail($email) !== null;
}
/** @return list<User> */
public function findAll(): array
{
return array_values($this->users);
}
public function count(): int
{
return count($this->users);
}
public function clear(): void
{
$this->users = [];
}
}
Order Repository with Queries
<?php
declare(strict_types=1);
namespace Tests\Fake;
use App\Domain\Order\Order;
use App\Domain\Order\OrderId;
use App\Domain\Order\OrderStatus;
use App\Domain\Order\OrderRepositoryInterface;
use App\Domain\Customer\CustomerId;
use DateTimeImmutable;
final class InMemoryOrderRepository implements OrderRepositoryInterface
{
/** @var array<string, Order> */
private array $orders = [];
public function save(Order $order): void
{
$this->orders[$order->id()->toString()] = $order;
}
public function findById(OrderId $id): ?Order
{
return $this->orders[$id->toString()] ?? null;
}
public function delete(Order $order): void
{
unset($this->orders[$order->id()->toString()]);
}
/** @return list<Order> */
public function findByCustomer(CustomerId $customerId): array
{
return array_values(array_filter(
$this->orders,
fn(Order $order) => $order->customerId()->equals($customerId)
));
}
/** @return list<Order> */
public function findByStatus(OrderStatus $status): array
{
return array_values(array_filter(
$this->orders,
fn(Order $order) => $order->status() === $status
));
}
/** @return list<Order> */
public function findPending(): array
{
return $this->findByStatus(OrderStatus::Pending);
}
/** @return list<Order> */
public function findCreatedBefore(DateTimeImmutable $date): array
{
return array_values(array_filter(
$this->orders,
fn(Order $order) => $order->createdAt() < $date
));
}
/** @return list<Order> */
public function findAll(int $limit = 100, int $offset = 0): array
{
return array_slice(array_values($this->orders), $offset, $limit);
}
public function count(): int
{
return count($this->orders);
}
public function countByStatus(OrderStatus $status): int
{
return count($this->findByStatus($status));
}
public function clear(): void
{
$this->orders = [];
}
// Test helpers
public function getAll(): array
{
return $this->orders;
}
public function has(OrderId $id): bool
{
return isset($this->orders[$id->toString()]);
}
}
Repository with Specifications
<?php
declare(strict_types=1);
namespace Tests\Fake;
use App\Domain\Product\Product;
use App\Domain\Product\ProductId;
use App\Domain\Product\ProductRepositoryInterface;
use App\Domain\Shared\Specification\SpecificationInterface;
final class InMemoryProductRepository implements ProductRepositoryInterface
{
/** @var array<string, Product> */
private array $products = [];
public function save(Product $product): void
{
$this->products[$product->id()->toString()] = $product;
}
public function findById(ProductId $id): ?Product
{
return $this->products[$id->toString()] ?? null;
}
public function delete(Product $product): void
{
unset($this->products[$product->id()->toString()]);
}
/** @return list<Product> */
public function findBySpecification(SpecificationInterface $spec): array
{
return array_values(array_filter(
$this->products,
fn(Product $product) => $spec->isSatisfiedBy($product)
));
}
/** @return list<Product> */
public function findAll(): array
{
return array_values($this->products);
}
public function clear(): void
{
$this->products = [];
}
}
Other Fake Implementations
Collecting Event Dispatcher
<?php
declare(strict_types=1);
namespace Tests\Fake;
use Psr\EventDispatcher\EventDispatcherInterface;
final class CollectingEventDispatcher implements EventDispatcherInterface
{
/** @var list<object> */
private array $events = [];
public function dispatch(object $event): object
{
$this->events[] = $event;
return $event;
}
/** @return list<object> */
public function dispatchedEvents(): array
{
return $this->events;
}
/** @return list<object> */
public function dispatchedEventsOf(string $eventClass): array
{
return array_values(array_filter(
$this->events,
fn(object $event) => $event instanceof $eventClass
));
}
public function hasDispatched(string $eventClass): bool
{
return count($this->dispatchedEventsOf($eventClass)) > 0;
}
public function clear(): void
{
$this->events = [];
}
}
Collecting Mailer
<?php
declare(strict_types=1);
namespace Tests\Fake;
use App\Infrastructure\Email\MailerInterface;
use App\Infrastructure\Email\EmailMessage;
final class InMemoryMailer implements MailerInterface
{
/** @var list<EmailMessage> */
private array $sent = [];
public function send(EmailMessage $message): void
{
$this->sent[] = $message;
}
/** @return list<EmailMessage> */
public function sentMessages(): array
{
return $this->sent;
}
/** @return list<EmailMessage> */
public function sentTo(string $email): array
{
return array_values(array_filter(
$this->sent,
fn(EmailMessage $msg) => $msg->to === $email
));
}
public function hasNotSentAny(): bool
{
return empty($this->sent);
}
public function clear(): void
{
$this->sent = [];
}
}
Frozen Clock
<?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 static function now(): self
{
return new self(new DateTimeImmutable());
}
public function advance(string $interval): self
{
return new self($this->now->modify($interval));
}
}
Usage in Tests
final class PlaceOrderHandlerTest extends TestCase
{
private PlaceOrderHandler $handler;
private InMemoryOrderRepository $orderRepository;
private InMemoryProductRepository $productRepository;
private CollectingEventDispatcher $eventDispatcher;
protected function setUp(): void
{
$this->orderRepository = new InMemoryOrderRepository();
$this->productRepository = new InMemoryProductRepository();
$this->eventDispatcher = new CollectingEventDispatcher();
$this->handler = new PlaceOrderHandler(
$this->orderRepository,
$this->productRepository,
$this->eventDispatcher
);
}
public function test_places_order(): void
{
// Arrange
$product = ProductMother::book();
$this->productRepository->save($product);
// Act
$orderId = $this->handler->handle(new PlaceOrderCommand(
customerId: 'customer-123',
items: [['productId' => $product->id()->toString(), 'quantity' => 2]]
));
// Assert - check repository
$order = $this->orderRepository->findById(OrderId::fromString($orderId));
self::assertNotNull($order);
// Assert - check events
self::assertTrue($this->eventDispatcher->hasDispatched(OrderPlacedEvent::class));
}
}
Generation Instructions
-
Read the repository interface:
- Extract all method signatures
- Identify entity type
- Identify ID type
-
Generate InMemory implementation:
- Array storage keyed by ID
- Implement all interface methods
- Add
clear()for test cleanup
-
Handle complex queries:
- Use
array_filterfor criteria - Support specifications if used
- Implement pagination with
array_slice
- Use
-
Add test helpers (optional):
getAll()— access internal statehas(Id $id)— check existencecount()— entity count
-
File placement:
tests/Fake/InMemory{Entity}Repository.php- Or
tests/Double/directory
Best Practices
- Match interface exactly — same method signatures
- Isolate per test — use
clear()in tearDown - Avoid complexity — simple in-memory logic
- Document deviations — if behavior differs from real impl
- Consider thread safety — for parallel tests (usually not needed)
微信扫一扫