First, what do I mean by "test Cashier?"
I'm not saying we need to test how Cashier works internally and how it interacts with Stripe. The Cashier package has its own extensive test suite, which I trust.
So when I say "test Cashier", I mean test how our application interacts with it.
For example, if my controller action takes a newly-created user and also sets them up as a Stripe customer, I want to make sure the expected method was called in Cashier, and for the expected user. That's it.
The Cashier testing section in the docs recommends using a Stripe sandbox account and making real network requests as part of your feature tests.
While I do agree that making the real call gives the greatest confidence, I prefer to isolate those tests into their own External test suite and keep my feature tests isolated from the outside world.
So how do we accomplish this with Cashier?
We can't mock Cashier, since its methods are introduced as traits on our models.
Trying to mock the User
model is going to be an exercise in frustration, so I don't recommend that.
Another approach is to mock the underlying StripeClient
that Cashier uses, but now we'll have to create a bunch of detailed mock responses, which is a pain to set up and will be brittle to maintain over releases.
Instead, the solution I landed on is very simple to set up and maintain, though it does require a minor change to the developer experience of using Cashier.
I create my own StripeService
class which is responsible for all interactions with Cashier that involve API calls to Stripe.
You can identify those methods by looking for calls chained on $this->stripe()
in the Cashier method.
And I don't need to implement every possible Cashier method that interacts with Stripe, but only the ones my application is actually using.
Here's a basic example of what that looks like for the scenario I mentioned at the start of this article:
// app/Services/CashierService.php
class StripeService
{
public function createAsStripeCustomer(User $user, ...$args)
{
return $user->createAsStripeCustomer(...$args);
}
}
// app/Http/Controllers/UserController.php
public function store(UserStoreRequest $request, StripeService $stripeService)
{
$user = User::create($request->validated());
// the old method using Cashier directly
$user->createAsStripeCustomer();
// the new method using my StripeService
$stripeService->createAsStripeCustomer($user);
}
Now, I can do normal mocking in my user tests, since I fully own the StripeService
and it has a very simple set of methods.
This simple change allows me to test my application without making network requests, while still using Cashier as intended.
Tomorrow, I will share a secondary benefit of this approach to testing Cashier.
Here to help,
Joel
P.S. Tinkerwell is a great tool for poking around Laravel and figuring out how it works. Check it out, and support Mastering Laravel at the same time.