Creating custom Facade fakes in Laravel
I recently released a new version of my Cloudflare Turnstile package for Laravel.
One of the features I added was the ability to test your Turnstile integrations using a fake implementation of the Turnstile client.
it('validates data', function () {
Turnstile::fake();
});
Under the hood, the package uses a custom Facade to call methods on the Turnstile client. This facade binds to a ClientInterface implemented by the concrete Client class as well as the FakeClient class.
class Turnstile extends Facade
{
protected static function getFacadeAccessor(): string
{
return ClientInterface::class;
}
}
The concrete Client class is then bound to the ClientInterface through the service container.
$this->app->scoped(ClientInterface::class, static function (Application $app): Client {
return new Client($app['config']->get('services.turnstile.secret'));
});
The power of the interface here is that it allows us to easily swap out the implementation with a FakeClient class when testing.
To achieve this, I added a fake() method to the Turnstile facade that calls the Facade class's static::swap() method.
class Turnstile extends Facade
{
protected static function getFacadeAccessor(): string
{
return ClientInterface::class;
}
public static function fake(): FakeClient
{
static::swap($fake = new FakeClient);
return $fake;
}
}
The swap() method itself does some work under the hood to replace the resolved instance of the facade itself with the provided fake implementation, but also overwrites the binding in the service container so that any subsequent calls
to the facade or any bindings to ClientInterface will return the fake implementation.
This allows us to easily stub out the Turnstile methods that are called by the package and provide clean, Laravel-esque APIs for testing.
Turnstile::fake(); // Force pass.
Turnstile::fake()->fail(); // Force fail.
Turnstile::fake()->expired(); // Force expired.
Why not give this a go in your own applications and packages? Future you will thank you for the improved developer experience and reduced boilerplate code!