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);
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();
Not all facades implement this method but the objective of this article is to understand what it does and implement it ourselves.
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);
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);
});
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;
}
Step 2: Create our Facade
class PaymentProvider extends Facade
{
public function getFacadeAccessor()
{
return PaymentProviderContract::class;
}
}
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();
}
}
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,
)
}
}
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);
}
}
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;
}
}
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);
});
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)
Really Nice!!