There's a moment in every project where someone wraps every dependency in a mock and calls it "unit testing." The test suite is fast, green, and completely useless.
The Problem with Over-Mocking
When you mock everything, you're not testing behavior — you're testing that your code calls methods in a certain order. That's a test for your implementation, not your domain.
// This test tells you nothing useful
$repository = $this->createMock(OrderRepository::class);
$repository->expects($this->once())
->method('save')
->with($this->isInstanceOf(Order::class));
$handler = new PlaceOrderHandler($repository);
$handler->handle(new PlaceOrder('SKU-001', 3));
If you rename save to persist, this test breaks — even though behavior hasn't changed. If you introduce a bug in how the order is constructed, this test passes anyway.
When Mocks Make Sense
Mocks are appropriate at system boundaries: HTTP clients, email gateways, payment providers. Anything that's slow, expensive, or has side effects outside your process.
But for your own domain objects, repositories backed by SQLite, or in-memory implementations? Just use the real thing.
In-Memory Implementations Over Mocks
final class InMemoryOrderRepository implements OrderRepository
{
/** @var array<string, Order> */
private array $orders = [];
public function save(Order $order): void
{
$this->orders[$order->id()->toString()] = $order;
}
public function ofId(OrderId $id): Order
{
return $this->orders[$id->toString()]
?? throw OrderNotFound::withId($id);
}
}
This is a real implementation. It honors the contract. It catches actual bugs. And it's still fast.
The Guideline
Mock at the boundary, use real implementations everywhere else. Your tests should break when behavior changes — not when you refactor internals.