Deploy E-Commerce Site to Netlify and Test Using GitHub Actions

How to test a site deployed to Netlify by starting a GitHub Actions workflow.

This blog post shows how to deploy a Netlify e-commerce site. The site uses Netlify functions and Stripe payment system. To ensure the site works, we will run a full end-to-end Cypress test after each deploy by triggering a GitHub workflow.

The deployed and tested e-commerce site

Repo

I have cloned a simple e-commerce site sdras/ecommerce-netlify to bahmutov/ecommerce-netlify and set up deploying to Netlify. You can find the site at tested-ecommerce-store.netlify.app and its deploy logs at Netlify. In this blog post I will show how to write a simple end-to-end test for the site. The test should run after each preview and production deploy to make sure the site's customer can actually buy items from the online store.

Or at least pretend they do - the site does no-op after the Stripe payment.

The interesting part is triggering the CI build after Netlify deploys the site. Instead of testing on Netlify via netlify-plugin-cypress build plugin, we are going to trigger our GitHub Actions workflow by using the netlify-plugin-github-dispatch. Running the tests on GitHub Actions allows us to run multiple test jobs in parallel, use Chrome or Firefox browser, and run tests on every major operating system. The Netlify Build is still limited to a single container and, compared to GitHub Actions, is more expensive.

End-to-end test

I will install Cypress and write a few basic tests. The most important test goes through the entire workflow: selects a few items, goes to the cart page, enters the credit card number, and ends up on the "thank you for your order" page. In this blog post I am using Cypress v6.5.0.

cypress/integration/spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/// <reference types="cypress" />
describe('e-commerce site', () => {
it('sells two items', () => {
cy.visit('/')
cy.contains('nav li', 'Men').click()

cy.log('**men\' items**')
cy.location('pathname').should('equal', '/men')

// let's not go crazy here, $30 is plenty!
// we change the max value by setting the value
// of the slider and then triggering the "input" event
cy.get('#pricerange').invoke('val', 30).trigger('input')

// check the number of items < $30
cy.get('.content .item').should('have.length', 3)
.first()
.click()

cy.log('**item page**')
cy.location('pathname').should('match', /\/product\//)
cy.contains('.product-info h1', 'Armin Basilio')
cy.get('.size-picker').select('Large')
// this jacket is so lovely, let's order 2
cy.contains('.update-num', '+').click()
cy.get('.quantity input[type=number]').should('have.value', 2)
cy.contains('.purchase', 'Add to Cart').click()

cy.contains('nav .carttotal', 2) // the little badge is present
// spy on the Netlify function call
cy.intercept({
method: 'POST',
pathname: '/.netlify/functions/create-payment-intent'
}).as('paymentIntent')
cy.contains('nav li', 'Cart').click()
cy.log('**cart page**')
cy.wait('@paymentIntent')
.its('response.body')
.then(JSON.parse)
.should('have.property', 'clientSecret')
// always confirm the total before paying
cy.contains('.total .golden', '$41.98')

cy.log('**fill payment**')
cy.get('input#email').type('[email protected]')

// working with cross-origin Stripe iframe
// https://www.cypress.io/blog/2020/02/12/working-with-iframes-in-cypress/
const ccNumber = '4242424242424242'
const month = '12'
const year = '30'
const cvc = '123'
const zipCode = '90210'
getIframeBody('.stripe-card iframe')
.find('input[name=cardnumber]').type(`${ccNumber}${month}${year}${cvc}${zipCode}`)
cy.contains('.pay-with-stripe', 'Pay with credit card').click()
// if the payment went through
cy.contains('.success', 'Success!').should('be.visible')

// automatically resets to empty cart
cy.scrollTo('top')
cy.get('nav .carttotal', {timeout: 6000}).should('not.exist') // the little badge is gone
cy.contains('Your cart is empty, fill it up!').should('be.visible')
})

/**
* Little utility to find an iframe
* @see https://www.cypress.io/blog/2020/02/12/working-with-iframes-in-cypress/
*/
const getIframeBody = (iframeSelector) => {
// get the iframe > document > body
// and retry until the body element is not empty
return cy
.get(iframeSelector)
.its('0.contentDocument.body').should('not.be.empty')
// wraps "body" DOM element to allow
// chaining more Cypress commands, like ".find(...)"
// https://on.cypress.io/wrap
.then(cy.wrap)
}
})

The test runs for 10 seconds - most of it waiting for the cart to reset to empty after the successful purchase.

The purchase user journey

Let's run this test on CI.

Tip: I am skipping writing more tests in favor of showing the entire testing setup in this blog post. But one thing I would add is going through the same test with mobile viewport to make sure the site works for users using smaller screens. See Use meaningful smoke tests for example. I would also immediately add a few visual tests. A full page screenshot at each major step along the customer journey would ensure we do not accidentally deploy a site with layout or styles problems.

Deploying to Netlify

We will build the static site and deploy using Netlify Build process. Since we are NOT going to run Cypress on Netlify, there is no point in installing it. We can set skip installing by setting the CYPRESS_INSTALL_BINARY=0 environment variable under the "Deploy Settings" on Netlify or in our netlify.toml file.

The dispatch plugin

At the end of the deploy we will trigger a GitHub Actions workflow in this repository, allowing the Workflow to install Cypress and run E2E tests against the deployed URL. To trigger the GitHub workflow we need to use the Netlify Build plugin netlify-plugin-github-dispatch

1
2
3
$ yarn add -D netlify-plugin-github-dispatch
info Direct dependencies
└─ [email protected]

We need to create a GitHub token with "repo" permissions so that the plugin can create the new workflow run. Keep this token secret and set it as environment variable GITHUB_TOKEN under Netlify build settings "Environment"

Set the GITHUB_TOKEN in Netlify Build environment

And then add the plugin to the netlify.toml file. Here is our project's netlify.toml file.

netlify.toml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[build]
command = "yarn generate"
functions = "functions"
publish = "dist"
[build.environment]
# skip installing Cypress during the build step
# because we will run Cypress tests in the GitHub workflow
CYPRESS_INSTALL_BINARY = "0"

[[plugins]]
package = "netlify-plugin-github-dispatch"
[plugins.inputs]
owner = "bahmutov"
repo = "ecommerce-netlify"
workflow = ".github/workflows/e2e.yml"

We have provided the inputs object with configuration. We basically just pointed back at the repository. But here it a pro tip: we could have triggered a GitHub Actions workflow in any repository at the end of the Netlify deploy, as long as we have the right permissions! Thus our end-to-end tests could reside in a separate repository from our web application. I personally prefer to keep the tests and the app in the same repository; it helps keep them in sync.

The workflow

We need a workflow to trigger at the end of the Netlify deploy. GitHub workflows can be triggered by different events, push and pull_request being the most common. We will use workflow_dispatch, which is a special event that can easily be triggered by the outside services or even manually.

We should create the workflow using the same relative path as entered in the dispatch inputs, in our case it will be file .github/workflows/e2e.yml.

Notice how we set up the workflow_dispatch inputs - we need to receive the deployPrimeUrl parameter - that's the Netlify URL our plugin will send. A good tip is to print it later to make it obvious in the CI logs using echo "Testing url ${{ github.event.inputs.deployPrimeUrl }}" command.

.github/workflows/e2e.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# test the deployed Netlify site
name: e2e
on:
workflow_dispatch:
inputs:
siteName:
description: Netlify Site Name
required: false
deployPrimeUrl:
description: Deployed URL
required: true
jobs:
show-event:
runs-on: ubuntu-20.04
steps:
- run: echo "Current Git info $GITHUB_WORKFLOW $GITHUB_REF $GITHUB_SHA"
- run: echo "Testing url ${{ github.event.inputs.deployPrimeUrl }}"

- name: Checkout ⬇️
uses: actions/checkout@v2

# test the deployed site using
# https://github.com/cypress-io/github-action
- name: Cypress run 🏃‍♀️
uses: cypress-io/github-action@v2
with:
config: baseUrl=${{ github.event.inputs.deployPrimeUrl }}

We are using the Cypress GitHub Action to install, cache, and execute Cypress. The only thing we need to configure is the baseUrl option to point at the deploy URL.

GitHub Actions run

Great! The tests have passed, the preview deploy is working.

Record to Cypress Dashboard

How does our site look during testing? What happens if a test fails? The simplest way to find out is to record the test results to Cypress Dashboard. From Cypress running locally, click the "Runs" tab.

Set up project to record to Cypress Dashboard

Follow the steps and store the created private recording key under the repository's secrets.

GitHub Actions run

Tip: I keep it simple and use the same name for the secret as for the environment variable.

Now let's modify our workflow file to tell Cypress to record the test results to Cypress Dashboard.

1
2
3
4
5
6
7
8
9
# test the deployed site using
# https://github.com/cypress-io/github-action
- name: Cypress run 🏃‍♀️
uses: cypress-io/github-action@v2
with:
record: true
config: baseUrl=${{ github.event.inputs.deployPrimeUrl }}
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

Let's open a pull request and we should see the Dashboard run URL in the Cypress output

Recorded run shows the Dashboard run URL

We can go the Dashboard and see the test results, view the video of the run, etc.

Cypress Dashboard plays the video of the Test Runner

You can see the recorded test results yourself by clicking on this badge

ecommerce-netlify

Status check

Notice that our pull request does not show the E2E workflow's status amongst the checks. Only the Netlify checks are present.

GitHub pull request shows only Netlify checks

This is because we do not associate the triggered workflow with the pull request. The best way I know to comment on the pull request with test status is Cypress GitHub Integration app. And I am not saying this just because I work at Cypress - no, it really makes sense to just flip the switch.

Scroll in the Cypress Dashboard project settings to "GitHub Integration"

GitHub Integration

Pick the repo from the dropdown list. If the repo is not listed, you need to adjust the Cypress GitHub App's permissions under your GitHub settings and allow the app to access the repository.

After selecting the "bahmutov/ecommerce-netlify" repository, I flip the switch.

Turn on Cypress GH Integration

Tip: Cypress Dashboard has GitLab and Bitbucket integrations too!

Let's test the integration. Let's open a pull request with a failing test. Netlify deploys the site, and GitHub workflow fails. We can see why in the test results on the Dashboard, plus there is the screenshot at the moment of failure, and the video of the tet run.

Looking at the test output and failed screenshot at Cypress Dashboard

Now let's go back to the pull request. Notice the red Cypress status check - this pull request should not be merged! The Cypress integration also comments on the pull request. The comment has the error and even the error screenshot thumbnail.

Cypress GitHub Integration adds the test information to the pull request

By running a battery of end-to-end tests we can ensure that every pull request is works, and we are not merging code that would break the production site for our users.

Parallel tests

If our test set grows, running all tests on a single machine starts to take too long. In that case, we can easily parallelize the tests across multiple GitHub machines. Read the Cypress GH Action documentation and my blog post Split Long GitHub Action Workflow Into Parallel Cypress Jobs. With all we have done so far, running the job in parallel should be easy.

See also