How to test Stripe Checkout and Customer portal with Laravel Dusk
Tags: programming, acceptancetesting, browsertesting, laravel
Published: 2021-03-04 21:48:40

---
title: How to test Stripe Checkout and Customer portal with Laravel Dusk
published: true
date: 2021-03-04 06:44:13 UTC
tags: Programming,AcceptanceTesting,BrowserTesting,Laravel
canonical_url: https://carlalexander.ca/test-stripe-checkout-customer-portal-laravel-dusk/
---

![How to test Stripe Checkout and Customer portal with Laravel Dusk](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bzzfu9sonnna6jpa6mpe.png)

Recently, I added billing to [Ymir](https://ymirapp.com). It's a [Laravel](https://laravel.com) application, and adding payment functionality to it was a breeze with [Laravel Cashier](https://laravel.com/docs/master/billing). Cashier supports different payment gateways, but it was originally designed for Stripe. It's also what I'm using with Ymir.

Over the last year or so, Stripe has released two products to ease the engineering burden dealing with payments. First, there's [Checkout](https://stripe.com/docs/payments/checkout), which lets you not worry about building a payment page. Second, there's the [customer portal](https://stripe.com/docs/billing/subscriptions/customer-portal) which lets people manage their subscription and billing information.

Laravel Cashier has support for both Checkout and the Customer portal, which means you don't have to code any payment backend. But that doesn't mean that you don't want to test the integration between Stripe and your Laravel application. After all, payment is one of the most important part of your application!

To do that, you'll want to use [Laravel Dusk](https://laravel.com/docs/master/billing) to do browser testing for your application. If you're not doing Browser testing already, you should consider looking into it. You could use another framework, but Dusk is the standard in the Laravel ecosystem.

# Getting webhooks to work during tests

Doing browser testing with Stripe Checkout and the Customer portal comes with certain challenges. The biggest one is that these products communicate with your application via [webhooks](https://en.wikipedia.org/wiki/Webhook). When developing your Laravel application, you use the [Stripe CLI](https://stripe.com/docs/stripe-cli) to forward webhooks to it.

We'll also need to do that when we run our browser tests. So an important aspect of using Laravel Dusk is having the Stripe CLI to forward our webhooks during the tests. We need to automate that as part of the test suite.

A simple way to automate the use of the Stripe CLI is to run it as a background process during our tests. We can use the [PHPUnit](https://phpunit.de/) `setUp` and `tearDown` methods to do it. Whenever we need the Stripe CLI, the `setUp` method starts the process and then stops it in the `tearDown` method.

```php
namespace Tests\Browser;

use Symfony\Component\Process\Process;
use Tests\DuskTestCase;

class StripeTest extends DuskTestCase
{
    /**
     * @var Process
     */
    private $process;

    protected function setUp(): void
    {
        parent::setUp();
        
        $this->process = new Process(['stripe', 'listen', '--api-key', env('STRIPE_SECRET'), '--forward-to', sprintf('%s/stripe/webhook', env('APP_URL'))]);
        $this->process->start();
    }

    protected function tearDown(): void
    {
        parent::tearDown();

        $this->process->stop();
    }
}
```

Above is the base test case class you can use to scaffold the Stripe CLI. It uses the [Symfony Process component](https://symfony.com/doc/current/components/process.html) to run the process in the background. It runs until the `stop` method gets called in the `tearDown` method.

The code expects two environment variables to be present. First, there's the `STRIPE_SECRET` environment variable containing your Stripe secret key. This allows the Stripe CLI to login without you having to enter your email and password. (You can read more about it [here](https://stripe.com/docs/cli/api_keys).)

Second, there's the `APP_URL` environment variable. We use it to generate the URL to pass to the `--forward-to` option. The `--forward-to` URL is where the Stripe CLI will forward webhook requests from Stripe. The default route with Laravel Cashier is `/stripe/webhook` so we just prepend the `APP_URL` environment variable to it.

# Cleaning up the customer after a test

Another challenge with testing the Stripe Checkout and Customer portal is cleaning up after the tests. You don't want to end up with hundreds of customers in your Stripe test environment. So you need to set up your test classes so that they can clean up any customer they might have created.

A simple way to achieve this is by storing the created customer in your test class. Once the test completes, you delete the customer in Stripe. Here's one way you could do this.

```php
namespace Tests\Browser;

use App\Model\User;
use Stripe\Customer;
use Stripe\Exception\InvalidRequestException;
use Stripe\Stripe;
use Symfony\Component\Process\Process;
use Tests\DuskTestCase;

class StripeTest extends DuskTestCase
{
    /**
     * @var Process
     */
    private $process;

    /**
     * @var User
     */
    private $user;

    protected function setUp(): void
    {
        parent::setUp();

        $secret = env('STRIPE_SECRET');
        
        $this->process = new Process(['stripe', 'listen', '--api-key', $secret, '--forward-to', sprintf('%s/stripe/webhook', env('APP_URL'))]);
        $this->process->start();

        Stripe::setApiKey($secret);
    }

    protected function tearDown(): void
    {
        parent::tearDown();

        $this->process->stop();

        if ($this->user instanceof User) {
            try {
                (new Customer($this->user->stripe_id))->delete();
            } catch (InvalidRequestException $exception) {
            }
        }

        $this->user = null;
    }
}
```

Above is the updated code sample from earlier. We added a few more things to take care of the customer cleanup. If you'd rather do this clean up inside each of your tests, you don't have to add any of this code.

First, there's a new `user` property. It'll store the `User` model object that has the Stripe customer ID that we want to clean up. (If you're using another model to store the Stripe customer ID, you should store that model instead.) During your tests, if you create a `User` model, you want to assign it to that property instead of creating a variable for it.

Next, there's a minor change in the `setUp` method. We added code to initialize the Stripe PHP SDK by calling the `setApiKey` method. We pass it the API secret key which Laravel Cashier expects to be in `STRIPE_SECRET` environment variable by default.

After that, in the `tearDown` method, we added code to delete the customer in Stripe. We start by checking if the `user` property has a `User` object stored in it. If it does, we call the code to delete the customer in Stripe.

To delete the customer,  we create an instance of the Stripe `Customer` class using the ID stored in the `User` model. We then call the `delete` method. We put that code in a simple [try-catch](https://en.wikipedia.org/wiki/Exception_handling) block. This is so we can discard errors if the customer didn't exist in Stripe.

# Creating page classes for Stripe

This wraps up the base test class. Next, we want to look at what we need to create [page classes](https://laravel.com/docs/master/dusk#pages). Page classes are a good way to group actions  you want to perform on specific web pages. 

With Stripe, we need a page class for both the checkout page and the customer portal. Both will share some similar elements. To begin, let's create the checkout page class. (You can do this with the `php artisan dusk:page` command.)

```php
namespace Tests\Browser\Pages;

use Laravel\Dusk\Browser;

class Checkout extends Page
{
    /**
     * Get the URL for the page.
     */
    public function url(): string
    {
        return '/';
    }

    /**
     * Assert that the browser is on the page.
     */
    public function assert(Browser $browser)
    {
        $browser->assertPathIs($this->url());
    }

    /**
     * Get the element shortcuts for the page.
     */
    public function elements(): array
    {
        return [
            '@element' => '#selector',
        ];
    }
}
``` 

Above is what you'd get if you ran the command php artisan dusk:page Checkout. The command creates a Checkout class with some methods. We only care about the url and assert methods for this article so we won't discuss the elements method.

# Asserting external URLs

Now, by default, Dusk lets you test pages that aren't part of your Laravel application. You can just replace the value returned by `url` method by a full URL. You then use the method `assertUrlIs` to check the URL in the `assert` method.

The issue is that the Stripe URLs are dynamic. So the `assertUrlIs` method won't work to validate that we're on the right page. To fix this, we'll need to create a [macro](https://laravel.com/docs/master/dusk#browser-macros) to add a custom assertion method in the `Browser` class.

```php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Laravel\Dusk\Browser;

class DuskServiceProvider extends ServiceProvider
{
    /**
     * Register Dusk's browser macros.
     */
    public function boot()
    {
        Browser::macro('assertUrlBeginsWith', function (string $url) {
            $pattern = str_replace('\*', '.*', preg_quote($url, '/'));

            $segments = parse_url($this->driver->getCurrentURL());

            $currentUrl = sprintf(
                '%s://%s%s%s',
                $segments['scheme'],
                $segments['host'],
                Arr::get($segments, 'port', '') ? ':'.$segments['port'] : '',
                Arr::get($segments, 'path', '')
            );

            PHPUnit::assertThat(
                $currentUrl,
                new RegularExpression('/^'.$pattern.'/u'),
                "Actual URL [{$this->driver->getCurrentURL()}] does not begin with the expected URL [{$url}]."
            );

            return $this;
        });
    }
}
```

Above is the modified `DuskServiceProvider` class with the macro for the `assertUrlBeginsWith` method. It's a method that checks if the URL starts a specific way, but it doesn't have to be identical. 

The code itself is nearly identical to the `assertUrlIs` method. The only change was to remove the `$` in the [regular expression](https://carlalexander.ca/beginners-guide-regular-expressions/). This changes the regular expression from requiring an exact match to just having the string begin with a specific URL.

## Modifying the assert and url methods

With our macro created, we can go back to our `Checkout` class. We're going to fill in the `assert` and `url` methods. After the changes, our `Checkout` class will look like this:

```php
namespace Tests\Browser\Pages;

use Laravel\Dusk\Browser;

class Checkout extends Page
{
    /**
     * Assert that the browser is on the page.
     */
    public function assert(Browser $browser)
    {
        $browser->assertUrlBeginsWith($this->url());
    }

    /**
     * Get the URL for the page.
     */
    public function url()
    {
        return 'https://checkout.stripe.com/pay';
    }
}
```

The `url` method returns `https://checkout.stripe.com/pay` which is the URL that all Stripe Checkout links start with. The `assert` method passes that URL to our macroed `assertUrlBeginsWith` method to check the page URL starts with the URL. We then do the same thing for the Stripe customer portal.

```php
namespace Tests\Browser\Pages;

use Laravel\Dusk\Browser;

class CustomerPortal extends Page
{
    /**
     * Get the URL for the page.
     */
    public function url()
    {
        return 'https://billing.stripe.com/session';
    }

    /**
     * Assert that the browser is on the page.
     */
    public function assert(Browser $browser)
    {
        $browser->assertUrlBeginsWith($this->url());
    }
}
```

The `CustomerPortal` class is also nearly identical to the `Checkout` class. We just changed the URL returned by the `url` method. The customer portal URL starts with `https://billing.stripe.com/session`.

# Testing Stripe subscription

Now, we have all we need to write our browser tests. These tests are going to vary from one Laravel application to another. But we can go over a small hypothetical one that goes over some common operations you might want to do.

```php
namespace Tests\Browser;

use Symfony\Component\Process\Process;
use Tests\DuskTestCase;

class StripeTest extends DuskTestCase
{
    // ...

    public function testSubscribe()
    {
        $this->browse(function (Browser $browser) {
            $this->user = User::factory()->create();

            $this->assertTrue($this->user->subscriptions->isEmpty());

            $browser->loginAs($this->user)
                    ->goToBilling()
                    ->on(new Checkout())
                    ->enterValidCreditCard()
                    ->enterNameOnCreditCard($this->user->name)
                    ->enterAddress()
                    ->subscribe()
                    ->on(new CustomerPortal());

            $this->user = $this->user->refresh();

            $this->assertTrue($this->user->subscription()->active());
        });
    }
}
```

Above is the updated `StripeTest` class with our sample test called `testSubscribe`. It tests that a subscription gets created in our Laravel application when we enter a valid credit card in the Stripe Checkout. Once subscribed, we should get redirected to the customer portal. (You can do this by setting the checkout return URL to the [application URL that redirects to the customer portal](https://laravel.com/docs/master/billing#billing-portal).)

Walking through the test code, it starts by creating a `User` using its [model factory](https://laravel.com/docs/master/database-testing#defining-model-factories). We then do a quick assertion to check that the user has no subscriptions. You can skip this if you prefer.

## Browser actions

After that, we start our test by performing browser actions through the `Browser` class. First, we need to ensure that the browser is logged in by using the `loginAs` method. 

Then we have `goToBilling` method. This is a hypothetical method that represents whatever actions you'd need the browser to do to get to and click the Stripe checkout link in your Laravel application. Clicking that link should then send us to the Stripe checkout page.

We verify this by using the `on` method with the `Checkout` class we created. Because of the changes we did earlier, Dusk should correctly detect that the browser is on a Stripe checkout page.

## Checkout class methods

Once we know that we're on the Stripe checkout page, we need to enter the payment information. We do that with some [page methods](https://laravel.com/docs/master/dusk#page-methods). For clarity, we did one page method per form section. But if you'd prefer one for the entire process, that's fine too.

```php
namespace Tests\Browser\Pages;

use Laravel\Dusk\Browser;

class Checkout extends Page
{
    // ...

    /**
     * Enter an address.
     */
    public function enterAddress(Browser $browser)
    {
        $browser->select('#billingCountry', 'CA')
                ->type('billingPostalCode', 'H0H0H0');
    }

    /**
     * Enter the name on the credit card.
     */
    public function enterNameOnCreditCard(Browser $browser, string $name)
    {
        $browser->type('#billingName', $name);
    }

    /**
     * Enter a valid credit card.
     */
    public function enterValidCreditCard(Browser $browser)
    {
        $browser->type('#cardNumber', '4242424242424242')
                ->type('#cardExpiry', '0129')
                ->type('#cardCvc', '111');
    }

    /**
     * Press the subscribe button.
     */
    public function subscribe(Browser $browser)
    {
        $browser->press('Subscribe')
                ->pause(20000);
    }
}
``` 

Above is the updated `Checkout` class with the new page methods. Most of the values for the form are hardcoded. (Fun fact: `H0H 0H0` is the [Santa Claus' Canadian postal code](https://en.wikipedia.org/wiki/Postal_codes_in_Canada#Santa_Claus).) If you'd rather pass them as arguments, that's ok as well. You're really free to build out these methods in the way that makes the most sense to you.

It's worth noting that you can't change the `4242424242424242` credit card number. It's the valid Visa credit card number that Stripe will accept in test mode. (You can view all the different card numbers in the [Stripe documentation](https://stripe.com/docs/testing#cards).) The CVC number can be anything, and the expiry date only needs to be a future date.

Another thing that might be unusual is the call to the `pause` method after pressing the subscribe button. You don't have to put the pause there, but a pause is necessary. You need to give Stripe the time to process the request, but also send over the subscription information to your Laravel application via the Stripe CLI. 

20 seconds is enough time in my experience. I tried lower values, but sometimes the tests would fail. So be aware if you choose to put a smaller pause time.

## Redirecting to the customer portal

After you subscribe, you’ll notice that our test expects Stripe to redirect us to the customer portal. To do that, you need to set up a route in your application that redirects to the customer portal. The [Cashier documentation](https://laravel.com/docs/master/billing#billing-portal) shows how you can do that.

```php
use Illuminate\Http\Request;

Route::get('/billing-portal', function (Request $request) {
    return $request->user()->redirectToBillingPortal();
})->name('billing-portal');
```

You then want to use that route when creating your Stripe checkout link. 

```php
$checkout = Auth::user()->newSubscription('default', 'price_xxx')->checkout([
    'success_url' => route('billing-portal'),
    'cancel_url' => route('your-cancel-route'),
]);
```

## Verifying the subscription

Once we're done testing with the browser, we want to check that our Laravel app received the changes from the Stripe API. To do that, we need an up-to-date version of our user. So we need to reload the model using the `refresh` method. 

After that, we need to get the `Subscription` model for the user. If all worked well, you should have one. We then check that the subscription is active by asserting that the `active` method returns `true`.

# Test cancelling an active subscription

Lastly, we’ll look at a test using the customer portal. There are a few tests you can do there, but the one we’ll look at is cancelling an existing subscription. Below is the updated `StripeTest` class:

```php
namespace Tests\Browser;

use Symfony\Component\Process\Process;
use Tests\DuskTestCase;

class StripeTest extends DuskTestCase
{
    // ...

    public function testCancelSubscription()
    {
        $this->browse(function (Browser $browser) {
            $this->user = User::factory()->create();

            $this->user->newSubscription()
                       ->create('pm_card_visa');

            $this->assertFalse($this->user->subscription()->cancelled());

            $browser->loginAs($this->user)
                    ->goToBilling()
                    ->on(new CustomerPortalPage())
                    ->cancelSubscription();

            $this->user = $this->user->refresh();

            $this->assertTrue($this->user->subscription()->cancelled());
        });
    }
}
```

This test starts the same as the one to test the subscription checkout flow. We create a user using the model factory. 

But after that, we create a Stripe subscription using the `newSubscription` method followed by the `create` method. This will create a subscription in Stripe using the given payment method ID. `pm_card_visa` is the payment method ID of a Visa credit card in the Stripe test environment.

Once the Stripe subscription created, we do a quick sanity check to check that the subscription isn’t in a  cancelled state. Once that’s done, we can start performing the browser actions. There’s only one specific one for the customer portal for this test. It’s clicking on the cancel subscription button.

But first, we need to navigate to the customer portal. We’re going to use the `goToBilling` method again for that. Then we ensure that we're on the customer portal using the `on` method. Then we can cancel the subscription.

```php
namespace Tests\Browser\Pages;

use Laravel\Dusk\Browser;

class CustomerPortal extends Page
{
    // ...

    /**
     * Cancel an active subscription.
     */
    public function cancelSubscription(Browser $browser)
    {
        $browser->clickLink('Cancel plan')
                ->pause(5000)
                ->press('Cancel plan')
                ->pause(10000);
    }
}
```

Above is the `cancelSubscription` method. On the Stripe customer portal, you need to click on “Cancel plan” twice to cancel your subscription. The first time you’re clicking on a link and the second time it’s a button. A pause is necessary between both clicks to allow the site to react to the first press. 

The second pause is there for the same reason as the one for testing the subscription. It allows Stripe to communicate back to the Laravel application via the Stripe CLI.

The test also wraps up similarly as the subscription one. We reload the `User` model to load the changes from the Stripe. We then do an assertion to ensure that the subscription is in a cancelled state.

# A good foundation

So with this, you have enough to test a subscription with Stripe checkout and a cancellation using the customer portal. There are still a lot of other scenarios you might want (and should!) to test. But this should give you the foundation you need to make writing those tests a breeze!