Patterns & Practices

Amir Rustamzadeh

Head of DX Engineering

@amirrustam

Webcast Series

Kevin Old

DX Engineer

Patterns & Practices

Future Topics

Experimental component testing support

New upcoming networking stubbing API

Shadow DOM Support

Data seeding practices

Authentication w/ third-party auth providers

Test organization

Patterns & Practices

Webcast Series

Real-World App (RWA)

Customizing the Command Log

Programmatic Authentication

CI Setup Tips

Ask Questions

Go to slido.com

Use event code #cypatterns

A payment application to demonstrate real-world usage of

Cypress testing methods, patterns, and workflows.

Frontend

React

XState

Material UI

Backend

lowDB

Express

State Management

UI Framework

Component Library

API Server

Database

TypeScript

Easy & Quick Setup

Zero additional dependencies. Clone & Run.

Responsive Testing

Practices for testing across desktop & mobile viewports.

100% Code Coverage

Full coverage of backend & frontend code purely w/ E2E tests.

Cross-Browser Testing

All test suits run in Chrome & Firefox in CI.

Visual Regression Testing

Demonstrates image snapshotting in practice w/ Cypress Percy plugin.

CI Best Practices

Clear CI config w/ parallelization, code coverage reporting, etc.

Dashboard

Real World App test runs are

publicly available at

Open GitHub issues with questions and feedback.

Customizing The Command Log 

The Mighty Command Log

The Mighty Command Log

To scale your test suites as your app grows in functionality and complexity, it can be tremendously beneficial to:

Add helpful contextual details to the command log for easier debugging.

Minimize extraneous log "noise" to improve readability.

Customize control of command log visual display and DOM snapshotting.

cy.pickDateRange()
cy.setAmountRange()
cy.login()
Cypress.Commands.add("login", (username, password, rememberUser = false) => {
  const signinPath = "/signin";
  const log = Cypress.log({
    name: "login",
    displayName: "LOGIN",
    message: [`🔐 Authenticating | ${username}`],
    autoEnd: false,
  });

  cy.server();
  cy.route("POST", "/login").as("loginUser");
  cy.route("GET", "checkAuth").as("getUserProfile");

  cy.location("pathname", { log: false }).then((currentPath) => {
    if (currentPath !== signinPath) {
      cy.visit(signinPath);
    }
  });

  log.snapshot("before");

  cy.getBySel("signin-username").type(username);
  cy.getBySel("signin-password").type(password);

  if (rememberUser) {
    cy.getBySel("signin-remember-me").find("input").check();
  }

  cy.getBySel("signin-submit").click();
  cy.wait("@loginUser").then((loginUser: any) => {
    log.set({
      consoleProps() {
        return {
          username,
          password,
          rememberUser,
          userId: loginUser.response.body.user?.id,
        };
      },
    });

    log.snapshot("after");
    log.end();
  });
});
cy.login()

Control Log Display

const log = Cypress.log({
  name: "login",
  displayName: "LOGIN",
  message: [`🔐 Authenticating | ${username}`],
  autoEnd: false,
});

Control Log Display

const log = Cypress.log({
  name: "login",
  displayName: "LOGIN",
  message: [`🔐 Authenticating | ${username}`],
  autoEnd: false,
});

You can control when a command is marked as complete within the log.

Control Log Display

Cypress.Commands.add("login", (username, password, rememberUser = false) => {
  const signinPath = "/signin";
  const log = Cypress.log({
    name: "login",
    displayName: "LOGIN",
    message: [`🔐 Authenticating | ${username}`],
    autoEnd: false,
  });

  cy.server();
  cy.route("POST", "/login").as("loginUser");
  cy.route("GET", "checkAuth").as("getUserProfile");

  cy.location("pathname", { log: false }).then((currentPath) => {
    if (currentPath !== signinPath) {
      cy.visit(signinPath);
    }
  });

  log.snapshot("before");

  cy.getBySel("signin-username").type(username);
  cy.getBySel("signin-password").type(password);

  if (rememberUser) {
    cy.getBySel("signin-remember-me").find("input").check();
  }

  cy.getBySel("signin-submit").click();
  cy.wait("@loginUser").then((loginUser: any) => {
    log.set({
      consoleProps() {
        return {
          username,
          password,
          rememberUser,
          userId: loginUser.response.body.user?.id,
        };
      },
    });

    log.snapshot("after");
    log.end();
  });
});

Control when a command ends.

Adding context for easier debugging.

Customize Command Console Output

Cypress.Commands.add("login", (username, password, rememberUser = false) => {
  const signinPath = "/signin";
  const log = Cypress.log({
    name: "login",
    displayName: "LOGIN",
    message: [`🔐 Authenticating | ${username}`],
    autoEnd: false,
  });

  cy.server();
  cy.route("POST", "/login").as("loginUser");
  cy.route("GET", "checkAuth").as("getUserProfile");

  cy.location("pathname", { log: false }).then((currentPath) => {
    if (currentPath !== signinPath) {
      cy.visit(signinPath);
    }
  });

  log.snapshot("before");

  cy.getBySel("signin-username").type(username);
  cy.getBySel("signin-password").type(password);

  if (rememberUser) {
    cy.getBySel("signin-remember-me").find("input").check();
  }

  cy.getBySel("signin-submit").click();
  cy.wait("@loginUser").then((loginUser: any) => {
    log.set({
      consoleProps() {
        return {
          username,
          password,
          rememberUser,
          userId: loginUser.response.body.user?.id,
        };
      },
    });

    log.snapshot("after");
    log.end();
  });
});

Customize Command Console Output

Minimize Command Logging

Cypress.Commands.add("login", (username, password, rememberUser = false) => {
  const signinPath = "/signin";
  const log = Cypress.log({
    name: "login",
    displayName: "LOGIN",
    message: [`🔐 Authenticating | ${username}`],
    autoEnd: false,
  });

  cy.server();
  cy.route("POST", "/login").as("loginUser");
  cy.route("GET", "checkAuth").as("getUserProfile");

  cy.location("pathname", { log: false }).then((currentPath) => {
    if (currentPath !== signinPath) {
      cy.visit(signinPath);
    }
  });

  log.snapshot("before");

  cy.getBySel("signin-username").type(username);
  cy.getBySel("signin-password").type(password);

  if (rememberUser) {
    cy.getBySel("signin-remember-me").find("input").check();
  }

  cy.getBySel("signin-submit").click();
  cy.wait("@loginUser").then((loginUser: any) => {
    log.set({
      consoleProps() {
        return {
          username,
          password,
          rememberUser,
          userId: loginUser.response.body.user?.id,
        };
      },
    });

    log.snapshot("after");
    log.end();
  });
});

Minimize logging noise by disabling logging for specific or all commands.

Control DOM Snapshotting

Cypress.Commands.add("login", (username, password, rememberUser = false) => {
  const signinPath = "/signin";
  const log = Cypress.log({
    name: "login",
    displayName: "LOGIN",
    message: [`🔐 Authenticating | ${username}`],
    autoEnd: false,
  });

  cy.server();
  cy.route("POST", "/login").as("loginUser");
  cy.route("GET", "checkAuth").as("getUserProfile");

  cy.location("pathname", { log: false }).then((currentPath) => {
    if (currentPath !== signinPath) {
      cy.visit(signinPath);
    }
  });

  log.snapshot("before");

  cy.getBySel("signin-username").type(username);
  cy.getBySel("signin-password").type(password);

  if (rememberUser) {
    cy.getBySel("signin-remember-me").find("input").check();
  }

  cy.getBySel("signin-submit").click();
  cy.wait("@loginUser").then((loginUser: any) => {
    log.set({
      consoleProps() {
        return {
          username,
          password,
          rememberUser,
          userId: loginUser.response.body.user?.id,
        };
      },
    });

    log.snapshot("after");
    log.end();
  });
});
cy.login()
Cypress.Commands.add("login", (username, password, rememberUser = false) => {
  const signinPath = "/signin";
  const log = Cypress.log({
    name: "login",
    displayName: "LOGIN",
    message: [`🔐 Authenticating | ${username}`],
    autoEnd: false,
  });

  cy.server();
  cy.route("POST", "/login").as("loginUser");
  cy.route("GET", "checkAuth").as("getUserProfile");

  cy.location("pathname", { log: false }).then((currentPath) => {
    if (currentPath !== signinPath) {
      cy.visit(signinPath);
    }
  });

  log.snapshot("before");

  cy.getBySel("signin-username").type(username);
  cy.getBySel("signin-password").type(password);

  if (rememberUser) {
    cy.getBySel("signin-remember-me").find("input").check();
  }

  cy.getBySel("signin-submit").click();
  cy.wait("@loginUser").then((loginUser: any) => {
    log.set({
      consoleProps() {
        return {
          username,
          password,
          rememberUser,
          userId: loginUser.response.body.user?.id,
        };
      },
    });

    log.snapshot("after");
    log.end();
  });
});

Put it all together

Kevin Old

DX Engineer

Programmatic Authentication

Programmatic Auth Provider Pattern

Identify Provider API Token Endpoint

Store credentials locally (cookie, localStorage, etc)

Bypass Auth Redirect Flow (varies based on provider)

Cypress.Commands.add("loginByApi",
(username, password = Cypress.env("defaultPassword")) => {
  return cy.request("POST", `${Cypress.env("apiUrl")}/login`, {
    username,
    password,
  });
});
it("login as user", function () {
  cy.loginByApi(ctx.authenticatedUser!.username).then((response) => {
    expect(response.status).to.eq(200);
  });
});
cy.loginByApi()
Cypress.Commands.add("loginByAuth0Api", (username: string, password: string) => {
  cy.log(`Logging in as ${username}`);
  const client_id = Cypress.env("auth0_client_id");
  const client_secret = Cypress.env("auth0_client_secret");
  const audience = Cypress.env("auth0_audience");
  const scope = Cypress.env("auth0_scope");

  cy.request({
    method: "POST",
    url: `https://${Cypress.env("auth0_domain")}/oauth/token`,
    body: {
      grant_type: "password",
      username,
      password,
      // ...
    },
  }).then(({ body }) => {
    const claims: any = jwt.decode(body.id_token);
    
    // ...

    window.localStorage.setItem("auth0Cypress", JSON.stringify(item));

    cy.visit("/");
  });
});
cy.loginByAuth0Api()
import React, { useEffect } from "react";
import { useService, useMachine } from "@xstate/react";
import { makeStyles } from "@material-ui/core/styles";
import { CssBaseline } from "@material-ui/core";

import { snackbarMachine } from "../machines/snackbarMachine";
import { notificationsMachine } from "../machines/notificationsMachine";
import { authService } from "../machines/authMachine";
import AlertBar from "../components/AlertBar";
import { bankAccountsMachine } from "../machines/bankAccountsMachine";
import PrivateRoutesContainer from "./PrivateRoutesContainer";
import { useAuth0, withAuthenticationRequired } from "@auth0/auth0-react";

// @ts-ignore
if (window.Cypress) {
  // Expose authService on window for Cypress
  // @ts-ignore
  window.authService = authService;
}

const useStyles = makeStyles((theme) => ({
  root: {
    display: "flex",
  },
}));

const AppAuth0: React.FC = () => {
  const { isAuthenticated, user, getAccessTokenSilently } = useAuth0();
  const classes = useStyles();
  const [authState] = useService(authService);
  const [, , notificationsService] = useMachine(notificationsMachine);

  const [, , snackbarService] = useMachine(snackbarMachine);

  const [, , bankAccountsService] = useMachine(bankAccountsMachine);

  // @ts-ignore
  if (window.Cypress) {
    // eslint-disable-next-line react-hooks/rules-of-hooks
    useEffect(() => {
      const auth0 = JSON.parse(localStorage.getItem("auth0Cypress")!);
      authService.send("AUTH0", {
        user: auth0.body.decodedToken.user,
        token: auth0.body.access_token,
      });
    }, []);
  } else {
    // eslint-disable-next-line react-hooks/rules-of-hooks
    useEffect(() => {
      (async function waitForToken() {
        const token = await getAccessTokenSilently();
        authService.send("AUTH0", { user, token });
      })();
    }, [isAuthenticated, user, getAccessTokenSilently]);
  }

  const isLoggedIn =
    authState.matches("authorized") ||
    authState.matches("refreshing") ||
    authState.matches("updating");

  return (
    <div className={classes.root}>
      <CssBaseline />

      {isLoggedIn && (
        <PrivateRoutesContainer
          isLoggedIn={isLoggedIn}
          notificationsService={notificationsService}
          authService={authService}
          snackbarService={snackbarService}
          bankAccountsService={bankAccountsService}
        />
      )}

      <AlertBar snackbarService={snackbarService} />
    </div>
  );
};

//@ts-ignore
let appAuth0 = window.Cypress ? AppAuth0 : withAuthenticationRequired(AppAuth0);
export default appAuth0;
cy.loginByAuth0Api() - Application State

Auth0, AWS Cognito, Okta, and more

cy.loginByXstate()
Cypress.Commands.add("loginByXstate", (username, password = Cypress.env("defaultPassword")) => {
  const log = Cypress.log({
    name: "loginbyxstate",
    displayName: "LOGIN BY XSTATE",
    message: [`🔐 Authenticating | ${username}`],
    // @ts-ignore
    autoEnd: false,
  });

  cy.server();
  cy.route("POST", "/login").as("loginUser");
  cy.route("GET", "/checkAuth").as("getUserProfile");
  cy.visit("/signin", { log: false }).then(() => {
    log.snapshot("before");
  });
  
  // Cypress.Commands.add("loginByXstate" ... );
  cy.window({ log: false }).then((win) => win.authService.send("LOGIN", { username, password }));

  return cy.wait("@loginUser").then((loginUser) => {
    log.set({
      consoleProps() {
        return {
          username,
          password,
          // @ts-ignore
          userId: loginUser.response.body.user.id,
        };
      },
    });

    log.snapshot("after");
    log.end();
  });
});
// Application Code

import React, { useEffect } from "react";
import { useService, useMachine } from "@xstate/react";

import { authService } from "../machines/authMachine";

if (window.Cypress) {
  // Expose authService on window for Cypress
  window.authService = authService;
}
cy.switchUser()
cy.loginByXstate()
cy.logoutByXstate()
+
it("User C comments on a transaction between User A and User B; User A and B get notifications that User C commented", function () {
  
  // User C
  cy.loginByXstate(ctx.userC.username);
  // ...

  // User A
  cy.switchUser(ctx.userA.username);
  // ...

  // User B
  cy.switchUser(ctx.userB.username);
  // ...
 });

Programmatic Auth: Performant and Efficient

CI / CD

version: 2.1

linux-workflow: &linux-workflow
  jobs:
    - cypress/install:
        name: "Setup Linux"

    # Run API tests against backend server
    - cypress/run:
        name: "API Tests"

    # Run E2E tests in Chrome
    - cypress/run:
        name: "UI Tests - Chrome"

    # Run E2E tests in Chrome with mobile device viewport
    - cypress/run:
        name: "UI Tests - Chrome - Mobile"

    # Run E2E tests in Firefox
    - cypress/run:
        name: "UI Tests - Firefox"

    # Run E2E tests in Firefox with mobile device viewport
    - cypress/run:
        name: "UI Tests - Firefox - Mobile"

windows-workflow: &windows-workflow
  jobs:
    - cypress/install:
        name: "Setup Windows"

    # Run E2E tests in Windows in Electron
    - cypress/run:
        name: "UI Tests - Electron - Windows"

CI Configuration

# Run E2E tests in Chrome
- cypress/run:
    name: "UI Tests - Chrome"
    browser: chrome
    spec: cypress/tests/ui/*
    executor: with-chrome-and-firefox
    wait-on: "http://localhost:3000"
    yarn: true
    start: yarn start:ci
    record: true
    parallel: true
    parallelism: 5
    ci-build-id: ${CIRCLE_SHA1:0:8}
    group: "UI - Chrome - Mobile"
    requires:
      - Setup Linux
    post-steps:
      - report-coverage

Linux vs Windows

# Run E2E tests in Windows in Electron
- cypress/run:
    name: "UI Tests - Electron - Windows"
    spec: cypress/tests/ui/*
    executor:
    # executor comes from the "windows" orb
      name: win/default
      shell: bash.exe
    wait-on: "http://localhost:3000"
    yarn: true
    start: yarn start:ci
    record: true
    parallel: true
    parallelism: 5
    ci-build-id: ${CIRCLE_SHA1:0:8}
    group: "UI - Electron - Windows"
    requires:
      - Setup Windows
    post-steps:
      - report-coverage
    no-workspace: true
# Run E2E tests in Chrome with mobile device viewport
- cypress/run:
    name: "UI Tests - Chrome - Mobile"
    browser: chrome
    spec: cypress/tests/ui/*
    config: "viewportWidth=375,viewportHeight=667"
    executor: with-chrome-and-firefox
    wait-on: "http://localhost:3000"
    yarn: true
    start: yarn start:ci
    record: true
    parallel: true
    parallelism: 5
    ci-build-id: ${CIRCLE_SHA1:0:8}
    group: "UI - Chrome - Mobile"
    requires:
      - Setup Linux
     post-steps:
       - report-coverage

CI Configuration

# Run E2E tests in Chrome with mobile device viewport
- cypress/run:
    name: "UI Tests - Chrome - Mobile"
    browser: chrome
    spec: cypress/tests/ui/*
    config: "viewportWidth=375,viewportHeight=667"
    executor: with-chrome-and-firefox
    wait-on: "http://localhost:3000"
    yarn: true
    start: yarn start:ci
    record: true
    parallel: true
    parallelism: 5
    ci-build-id: ${CIRCLE_SHA1:0:8}
    group: "UI - Chrome - Mobile"
    requires:
      - Setup Linux
     post-steps:
       - report-coverage

CI + Cypress Dashboard = ❤️

# Run E2E tests in Chrome with mobile device viewport
- cypress/run:
    name: "UI Tests - Chrome - Mobile"
    browser: chrome
    spec: cypress/tests/ui/*
    config: "viewportWidth=375,viewportHeight=667"
    executor: with-chrome-and-firefox
    wait-on: "http://localhost:3000"
    yarn: true
    start: yarn start:ci
    record: true
    parallel: true
    parallelism: 5
    ci-build-id: ${CIRCLE_SHA1:0:8}
    group: "UI - Chrome - Mobile"
    requires:
      - Setup Linux
     post-steps:
       - report-coverage

GitHub PR Checks

# Run E2E tests in Chrome with mobile device viewport
- cypress/run:
    name: "UI Tests - Chrome - Mobile"
    browser: chrome
    spec: cypress/tests/ui/*
    config: "viewportWidth=375,viewportHeight=667"
    executor: with-chrome-and-firefox
    wait-on: "http://localhost:3000"
    yarn: true
    start: yarn start:ci
    record: true
    parallel: true
    parallelism: 5
    ci-build-id: ${CIRCLE_SHA1:0:8}
    group: "UI - Chrome - Mobile"
    requires:
      - Setup Linux
     post-steps:
       - report-coverage

GitHub PR Code Coverage Reporting

commands:
  report-coverage:
    description: |
      Store coverage report as an artifact and send it to Codecov service.
    steps:
      - store_artifacts:
          path: coverage
      - run: npx nyc report --reporter=text || true
      # Use Codecov Circle CI Orb for easy report uploads
      - codecov/upload:
          file: coverage/coverage-final.json

Code Coverage Reporting

# Setup
#  1. Install Cypress
#  2. Validate types
#  3. Run server unit-tests
- cypress/install:
    name: "Setup Linux"
    yarn: true
    executor: with-chrome-and-firefox
    post-steps:
      - run:
          name: Check Types
          command: yarn types
      - run:
          name: Run Unit Tests
          command: yarn test:unit:ci

Cypress Docker Images

executors:
  with-chrome-and-firefox:
    docker:
      - image: "cypress/browsers:node12.18.0-chrome83-ff77"

Open GitHub issues with questions and feedback.

Questions?

Amir Rustamzadeh

Head of DX Engineering

@amirrustam

Kevin Old

DX Engineer