FakeIt — Queue::fake() ergonomics for the classes Laravel forgot

"Give me much nicer syntax than Mockery for faking my own classes."

That's the email I got. And honestly — fair. Look at what we accept as normal:

public function test_it_notifies_the_team()
{
    $mailer = Mockery::mock(Mailer::class);
    $mailer->shouldReceive('send')->once()->with(Mockery::type(Message::class));
    $this->app->bind(Mailer::class, fn () => $mailer);

    // ...exercise the feature...

    Mockery::close();
}

protected function tearDown(): void
{
    Mockery::close();
    parent::tearDown();
}

The tearDown line is the tell. That Mockery::close() exists because the library can't trust your container to clean up — so you flush state by hand after every test or watch your fakes leak into the next one like a hungover houseguest.

FakeIt says no:

public function test_it_notifies_the_team()
{
    $mailer = FakeIt::of(Mailer::class)
        ->returns(['send' => true]);

    // ...exercise the feature...

    $mailer->assertCalled('send', fn ($message) => ...);
}

Three things to notice. First, the fake is a real Mailer — type hints stay honest, your IDE stops crying. Second, no Mockery::close(): fakes live in the container and reset when it rebuilds between tests, so leakage is structurally impossible rather than a matter of personal discipline. Third, you assert after the fact. Expectations-up-front mean a failure on line 3 of a 40-line test tells you nothing about whether the feature worked; a spy-style assertion at the end lets the test run to completion and report what actually happened.

The philosophy is sound, and — to be clear — not original. You're asserting that a collaborator was used inside a feature test, while you unit-test that collaborator's own behaviour elsewhere. That is precisely Laravel's native Queue::fake() + Queue::assertPushed() pattern. FakeIt just democratises it for the classes Laravel doesn't natively cover: your Mailer, your PaymentGateway, your WeirdThirdPartyClient.

Here's the key insight, and it explains why the API feels both familiar and slightly odd. Native fakes assert a classQueue::assertPushed(OrderShipped::class, fn ($job) => …) — plus optionally the args on the message object. They get away with that because a job has exactly one entry point (handle()) and an event is just data. There's nothing to name.

A general service has many methods, so FakeIt generalises: assertCalled('method', fn) names the method. Necessary. But for a single-method action — say ProcessTokenAccessAction::handle — forcing assertCalled('handle', fn ($token) => …) is redundant noise; the method name carries zero information. So FakeIt infers it:

ProcessTokenAccessAction::assertCalled(fn ($token) => ...);

That mirrors Queue::assertPushed(OrderShipped::class, fn ($job) => …) exactly. Same shape, same ergonomics, no invented ceremony. For single entry-point collaborators, FakeIt dissolves into Laravel's own idiom. For multi-method ones, it adds the one bit of information the native pattern doesn't need.

Now put the paint roller down and pick up a scalpel. I like this package, but four caveats before you fake everything.

One: "much nicer than Mockery" is qualified. Mockery still wins on argument matchers (Mockery::on, partial wildcard lists), proper spies, and — decisively — faking final and static methods. FakeIt can't touch those. Keep Mockery in the toolbox; this isn't a divorce.

Two: asserting a class was used couples your feature test to implementation. If the outcome already proves the wiring — you stub the token service to return a URL and then assertRedirect($url) — the extra assertCalled is brittle coupling that breaks when you refactor how the feature is built, not whether it works. Reserve usage assertions for side-effecty collaborators whose outcome can't reveal their own involvement: a mailer, an external API, a cache write.

Three: nicer syntax makes over-asserting frictionless. That's a tax paid forever, by every future reader of the test, for coupling that bought nothing. The cheapness is the trap.

Four — and this one will bite: the constructor-skipping default. FakeIt::of(Class::class) builds the fake without running the constructor. Usually fine. Catastrophic when the faked method reads constructor state — then you silently get nulls where your real object had a connection or a key. Reach for with([...]) to supply constructor args, or partial() plus the args, when the method under fake depends on what the constructor set up.

Verdict: ship it. Keep the philosophy — assert collaborator usage in feature tests, unit-test the collaborator separately. Assert at the entry-point level: the class for single entry points, the method for multi-method services. And the ordering matters — prove the behaviour first; prove the wiring only when behaviour can't.

Now delete the assertCalled from your next test and see if it got weaker, or just less coupled.