DEV Community

Cover image for Fake Facade in Laravel
Gabriel Oliveira
Gabriel Oliveira

Posted on • Updated on

Fake Facade in Laravel

Generally, when we work with services within our application that interact with the external world, for example: Sending emails, queued processing, payment providers, it can end up making it difficult to implement automated tests in our application, it is slow and unfeasible to trigger all emails Once your tests run, make a request to a payment gateway, trigger a job to your real queue.

In this article I will talk about an implementation pattern that gets around this problem by facilitating the implementation of automated tests with an example of a payment provider implementation.

What is a Facade?

Facade is a design pattern that allows us to implement a simple interface to abstract a complex system.

Laravel Examples

As users of the framework, we do not need to implement email triggering functionality from scratch, queue management, connection to the Database, this is already well abstracted by the framework using Facades.

Email trigger example

Mail::to('email@here.com')->send(new EmailContent);
Enter fullscreen mode Exit fullscreen mode

That said, how would we test whether the email is actually being fired using automated tests?

An alternative is to run the tests with our personal email and check our inbox, but that just thinking about it doesn't seem very efficient.

For that there is this Fake Facade pattern, whose name I invented because I couldn't find a name for it.

If you have already created tests for email dispatching, you probably already know the fake method of this facade:

Mail::fake();
Enter fullscreen mode Exit fullscreen mode

Not all facades implement this method but the objective of this article is to understand what it does and implement it ourselves.

Print of Mail class implementation in laravel

Looking at what the method does we can see that it basically calls the swap method which basically swaps and is behind this Facade.

So basically, before we call the method, the Mail class points to the real implementation of sending emails and after calling the method it points to this MailFake class.

What does this MailFake class do?

Firstly, it needs to implement all the methods that the real implementation has so that no problems occur during the execution of your program.

Let's go back to the example:

Mail::to('email@here.com')->send(new EmailContent);
Enter fullscreen mode Exit fullscreen mode

In the MailFake class, the send method actually just adds the email to an array so we can test this in the future, example:

function dispatchEmail() {
    Mail::to('email@here.com')->send(new EmailContent);
}

it('should dispatch email successfully', function () {
    // Arrange
    Mail::fake();

    // Act
    dispatchEmail();

    // Assert
    Mail::assertSent(EmailContent::class);
});
Enter fullscreen mode Exit fullscreen mode

Now that we understand what the implementation is for and how to use it, let's create our own implementation.

One case where this implementation is viable is integration with payment providers, let's go.

Step 1: Let's create our contract (interface)


interface PaymentProviderContract
{
    charge(Invoice $invoice): Charging;
}

Enter fullscreen mode Exit fullscreen mode

Step 2: Create our Facade


class PaymentProvider extends Facade
{
    public function getFacadeAccessor()
    {
        return PaymentProviderContract::class;
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 3: We create our actual payment provider implementation

class StripePaymentProvider implements PaymentProviderContract
{
    public function charge(Invoice $invoice): Charging
    {
        // Interacts with the stripe api
        return new Charging();
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: We point Facade towards implementing Stripe

We can do this in our AppServiceProvider in the register method.


class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->register(
            PaymentProviderContract::class,
            fn () => new StripePaymentProvider,
        )
    }
} 

Enter fullscreen mode Exit fullscreen mode

Now every time we call PaymentProvider::charge() it will point to the Stripe implementation because we declared this in our Service container.

Step 5: We create our fake implementation


class FakePaymentProvider implements PaymentProviderContract
{
    private array $chargings = [];

    public function charge(Invoice $invoice): Charging
    {
        $charging = new Charging;

                // Fill charging with invoice informations

        $this->chargings[] = $charging;
    }

    public function assertChargingsCount(int $count): void
    {
        expect($this->chargings)->toHaveCount($count);
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 6: Implement the method to perform the swap on our facade


class PaymentProvider extends Facade
{
    public static function fake(): FakePaymentProvider
    {
        static::swap($fake = new FakePaymentProvider)

        return $fake;
    }

    public function getFacadeAccessor()
    {
        return PaymentProviderContract::class;
    }
}

Enter fullscreen mode Exit fullscreen mode

Okay, now we can safely test our application


function chargeUser(User $user): Charging
{
    return PaymentProvider::charge($user);
}

it('should charge a user', function () {
    // Arrange
    PaymentProvider::fake(); // Swap for fake class

    // Act
    chargeUser(); // now the charge method will be executed by the fake class
    // Assert
    PaymentProvider::assertChargingsCount(1);
});
Enter fullscreen mode Exit fullscreen mode

Okay, now with this pattern implemented we can test only the rules of our application without needing to "test other people's code" which would mean testing Stripe's Api, for example.

We can and should create more auxiliary methods in our FakePaymentProvider class to improve the quality of development and also the effectiveness of our tests.

Top comments (1)

Collapse
 
pedrovian4 profile image
pedrovian4

Really Nice!!