Automate Node.js Website E2E Test with Cypress and GitHub Actions

Marko Cen
Updated: June 24, 2020
5 mins read
CC BY-ND 4.0

In this article, we are going to explain how to setup a continue integration job on GitHub Actions to automatically trigger end-to-end testing by using Cypress e2e framework.

TLDR, the working code can be found here.

Not Familiar with Cypress?

Cypress is an open sourced modern e2e testing framework. It provides out of box solution to test website functionalities with cross browsers support. Cypress leverages the most popular testing libraries, like Sinon, Chai, etc, if you are familiar with these libraries, you can start to write Cypress e2e tests with zero learning curve. Cypress also super easy to setup, you can literally setup an Cypress testing server in 5 minutes by following its official documents. In this article, we are going to use Cypress to test our production website.

Why GitHub Actions?

GitHub Actions is the latest CI/DI platform introduced by GitHub. It deeply integrated with GitHub source control, you can easily build, test, deploy your code right from the your GitHub code repository. It supports most major platforms, like popular Linux distros, Windows and MacOS. GitHub also offers 2,000 Actions minutes per month for free account, which it is enough for most side/small projects. You can simply define GitHub Action jobs by using a yaml files and commit it into source control.

Build Website

First we are going to build the testing website using nodejs and express, our website has one page and one API handler. When users visit the website, they will see a login form, and login request will be handled by /submit api.

import express from "express";
import * as http from "http";
import * as path from "path";

const app = express();

app.get("/", (req, res) => {
  res.sendFile(path.resolve("public/index.html"));
});

app.post("/submit", (req, res) => {
  res.end("submit success!");
});

const server = http.createServer(app);

server.listen("4040");
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Cypress E2E Test Demo Page</title>
  </head>
  <body>
    <!-- simple login form -->
    <form>
      <label>
        username
        <input type="text" id="username" />
      </label>
      <label>
        password
        <input type="password" id="password" />
      </label>
      <button type="submit">Submit</button>
    </form>

    <!-- we use axios as restful request client -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
    <script>
      const form = document.querySelector('form');

      form.onsubmit = (e) => {
        e.preventDefault();
        const username = form.querySelector('#username').value;
        const password = form.querySelector('#password').value;

        axios.post("/submit", { username, password });
      }
    </script>
  </body>
</html>

Setup E2E Test

First install Cypress through npm or yarn

npm install cypress -D

Then create cypress.json to config cypress server

{
  "baseUrl": "http://localhost:4040",
  "fixturesFolder": "e2e/fixtures",
  "integrationFolder": "e2e/tests",
  "screenshotsFolder": "e2e/screenshots",
  "videosFolder": "e2e/videos",
  "supportFile": false,
  "pluginsFile": false
}

We config the baseUrl to localhost because we are going to start the website in local CI environment. It also support remote url like using a deployed website url, in that case, e2e tests will be run against the production website. integrationFolder defined the path to test files, and fixturesFolder and screenshotsFolder will be used to locate fixture data and screenshot images.

The last thing is to write the test, for demonstrate purpose, we only have one test file which contains two tests to cover the login flow. You can check more usage details on cypress website.

// e2e/tests/index.test.js

describe("Index Page", () => {
  before(() => {
    cy.visit("localhost:4040");
  });

  it("should contain a form element", () => {
    const form = cy.get("form");
    form.should("exist");
    form.get('input#username').should('exist');
    form.get('input#password').should('exist');
    form.get('button[type="submit"]').should('exist');
  });

  it("should send http request with login payload when submit button clicked", () => {
    cy.server();
    cy.route("POST", "/submit").as("submit");
    cy.get('input#username').type("test");
    cy.get('input#password').type("123abc");
    cy.get('button[type="submit"]').click();

    cy.wait("@submit").should("have.property", "status", 200);

    cy.get("@submit").should((xhr) => {
      expect(xhr.request.body).to.deep.equal({ username: "test", password: "123abc" });
      expect(xhr.response.body).to.equal("submit success!");
    });
  });
});

Test Run

That's it, our Cypress server is ready to run, to start the server locally, execute npx cypress open, and the Cypress dashboard will show up

cypress-dashboard-screenshot

In dashboard, it lists the test we just wrote, we can select the testing browser and check previous running results. Now click 'Run all specs', Cypress will start to execute our tests (make sure our website is started before running tests!). We will see the target browser pop up and all tests executed successfully!

cypress-running-browser

Define CI Pipeline

The last step is to setup GitHub Action workflow, workflow is a custom automated processes that we can set up in repository to build, test, package, release, or deploy any project on GitHub. We want the workflow can run e2e test automatically when there are new commits pushed to master branch, the workflow has several jobs, first it will checkout source code to local, then install nodejs and dependencies, finally start e2e testing.

To define workflow, we should create a yaml file and put it in .github/workflows folder, and commit it into source control. More workflow syntax detail can be found on here.

name: E2E Test

on:
  push:
    branches:
      - master

jobs:
  e2e:
    runs-on: ubuntu-16.04

    steps:
      - name: Checkout Code
        uses: actions/checkout@v2

      - name: Install Node
        uses: actions/setup-node@v1
        with:
          node-version: "14"

      - name: Install Dependencies
        run: npm i

      - name: Run Cypress
        uses: cypress-io/github-action@v2
        with:
          start: npm run start

Here we use the official cypress-io/github-action plugin to help us start cypress testing, but it's same as easy as setting up by ourself. Once this file committed and pushed, we will see workflow can be triggered by following commits.

github-actions-run-results