Exploring the Cypress Real World App

@ceceliacreates

Hello! 👋🏽

Cecelia Martinez

  • Success Engineer @ Cypress.io 🌲
  • Atlanta, GA 🍑
  • Women Who Code Track Evangelist 👩🏽‍💻
  • Out in Tech Leadership Team 🌈
  • @ceceliacreates 

 

Why the Real World App?

  • Real-world usage of Cypress testing methods, patterns, and workflows
  • Clone, install, and run app without additional dependencies or native components
  • Learn, experiment, tinker, and practice

@ceceliacreates

RWA: Tech Stack

  • React
  • XState
  • Express
  • Lowdb
  • Material-UI
  • TypeScript

@ceceliacreates

App Functionality

  • Authentication (Sign up, Login, Logout)
  • Onboarding
  • Add/Delete Bank Accounts
  • Create Transactions
  • Accept or Reject Transaction Requests
  • Like and Comment on Transactions
  • View Transactions between Friends
  • Update Account Settings
  • View and Dismiss Notifications

@ceceliacreates

App Demo Video

Generated with cypress-movie plugin

  • Installation
  • File organization
  • Configuration
  • Using the app

Getting Started

@ceceliacreates

Installation

// Install the application
yarn install

// Run the app
yarn dev

// Start Cypress
yarn cypress:open

@ceceliacreates

File Organization

// cypress.json

"integrationFolder": "cypress/tests"

// Folder Structure within cypress/

fixtures
plugins
  > index.ts
support
  > commands.ts
  > index.ts
  > utils.ts
tests
  > api
  > ui

Configurations

// cypress.json

{
  "baseUrl": "http://localhost:3000",
  "projectId": "7s5okt",
  "integrationFolder": "cypress/tests",
  "viewportHeight": 1000,
  "viewportWidth": 1280,
  "firefoxGcInterval": null,
  "nodeVersion": "system",
  "env": {
    "apiUrl": "http://localhost:3001",
    "mobileViewportWidthBreakpoint": 414,
    "coverage": false,
    "codeCoverage": {
      "url": "http://localhost:3001/__coverage__"
    }
  }
}

Command Types

// global.d.ts

/*** Logs-in user by using API request */
loginByApi(username: string, password?: string): Chainable<Response>;

/*** Logs in bypassing UI by triggering XState login event */
loginByXstate(username: string, password?: string): Chainable<any>;

/*** Logs out via bypassing UI by triggering XState logout event */
logoutByXstate(): Chainable<void>;

cypress/global.d.ts

Additional Scripts

"start": "cross-env NODE_ENV=development concurrently yarn:start:react yarn:start:api"

"db:seed": "yarn tsnode scripts/generateSeedData"

"start:empty": "cross-env NODE_ENV=development EMPTY_SEED=true concurrently yarn:start:react yarn:start:api:watch"

"list:dev:users": "cat data/database.json | json -a users | json -a id username"

@ceceliacreates

Logging In

  • User list in data/database.json or data/database-seed.json
  • Default password: s3cret

@ceceliacreates

Testing Approach

  • API
  • UI
  • Unit

@ceceliacreates

API Tests

  • API tests were helpful in validating the back end before building the front end

  • Uses cy.request() to make request and then make assertion on the response

  • Makes sense to use Cypress for API tests in this case because we are spinning up a browser anyway

@ceceliacreates

API Tests

@ceceliacreates

context("GET /bankAccounts", function () {
  it("gets a list of bank accounts for user", function () {
    const { id: userId } = ctx.authenticatedUser!;
   cy.request("GET", `${apiBankAccounts}`).then((response) => {
     expect(response.status).to.eq(200);
     expect(response.body.results[0].userId).to.eq(userId);
    });
  });
});

UI Tests

UI tests are designed around the user views of the app

@ceceliacreates

UI Tests

  • Long tests along critical paths within those views
  • tests/ui/api-users.spec.ts - "should allow a visitor to sign-up, login, and logout" is a good example of long critical path test

@ceceliacreates

Unit Tests

​src/__tests__

// run unit tests
yarn test:unit
yarn test:unit:ci (no watch mode)

@ceceliacreates

Test Data Management

  • Approach
  • Database seeding
  • API Endpoint
  • Using Test Data

@ceceliacreates

Approach

  • Local JSON database managed with lowdb
  • Database reseeded when app is started AND between each Cypress test
  • Data is generated based on business logic of the app in scripts/seedDataUtils.ts

@ceceliacreates

Approach

  • No need to create business logic (example, certain types of transactions or user relationships) within the tests
  • May not be best approach for all data models
  • Goal here is to keep test code clear and simple as possible

@ceceliacreates

Database Seeding

cy.task("db:seed") in beforeEach hook, runs between every test

//plugins/index.ts

on("task", {
async "db:seed"() {
  // seed database with test data
  const { data } = await axios.post(`${testDataApiEndpoint}/seed`);
  return data;
  },
});

@ceceliacreates

API Endpoint

Created only for test data

// backend/testdata-routes.ts

router.post("/seed", (req, res) => {
  seedDatabase();
  res.sendStatus(200);
});

@ceceliacreates

API Endpoint

Created only for test data

// backend/database.ts

export const seedDatabase = () => {
  const testSeed = JSON.parse(
    fs.readFileSync(path.join(process.cwd(), "data", "database-seed.json"), "utf-8")
  );

  // seed database with test data
  db.setState(testSeed).write();
  return;
};

@ceceliacreates

Using Test Data

Custom commands for querying database

// support/commands.ts
 
Cypress.Commands.add("database", (operation, entity, query, logTask = false) => {
  const params = {
    entity,
    query,
  };

  return cy.task(`${operation}:database`, params, { log: logTask }).then((data) => {
    log.snapshot();
    log.end();
    return data;
  });
});

Using Test Data

Custom commands for querying database

// plugins/index.ts

on("task", {
    "filter:database"(queryPayload) {
      return queryDatabase(queryPayload, (data, attrs) => _.filter(data.results, attrs));
    },
    "find:database"(queryPayload) {
      return queryDatabase(queryPayload, (data, attrs) => _.find(data.results, attrs));
    },
  });

@ceceliacreates

Using Test Data

Custom commands for querying database

// plugins/index.ts

const queryDatabase = ({ entity, query }, callback) => {
    const fetchData = async (attrs) => {
      const { data } = await axios.get(`${testDataApiEndpoint}/${entity}`);
      return callback(data, attrs);
    };
    return Array.isArray(query) ? Promise.map(query, fetchData) : fetchData(query);
  };

@ceceliacreates

Custom Commands

// Selector Commands

cy.getBySel()

return cy.get(`[data-test=${selector}]`, ...args);

cy.getBySelLike()

return cy.get(`[data-test*=${selector}]`, ...args);

@ceceliacreates

cypress/support/commands.ts

Custom Commands

// Login Commands

cy.login(username, password, rememberUser)

cy.loginByApi(username, password)

cy.loginByXstate(username, password)

cy.switchUser(username) 

@ceceliacreates

cypress/support/commands.ts

Custom Commands

// Transaction Commands

cy.createTransaction(payload)

cy.setTransactionAmountRange(min, max)

// Navigation/Function Commands

cy.nextTransactionFeedPage(service, page)

cy.pickDateRange(startDate, endDate)

@ceceliacreates

cypress/support/commands.ts

Continuous Integration

@ceceliacreates

Circle CI

@ceceliacreates

Cypress Dashboard

  • CI runs recorded to Cypress Dashboard
  • https://dashboard.cypress.io/projects/7s5okt/runs
  • View screenshots, videos, and output of failed runs
  • Sort runs by branch, status, committer, time range, and tags
  • View analytics like run duration and test suite size

@ceceliacreates

Responsive Testing

No separate UI tests for mobile

// package.json

"cypress:open:mobile": "cypress open --config viewportWidth=375,viewportHeight=667"

"cypress:run:mobile": "cypress run --config viewportWidth=375,viewportHeight=667"

@ceceliacreates

Responsive Testing

# 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
        group: "UI - Chrome - Mobile"
        requires:
          - Setup Linux
        post-steps:
          - report-coverage

Code Coverage

// Generate Code Coverage report

yarn cypress:run --env coverage=true

@ceceliacreates

Code Coverage

// codecov.yml

codecov:
  require_ci_to_pass: yes

coverage:
  precision: 2
  round: nearest
  range: "90...100"

@ceceliacreates

Overview

  • Why the Real World App?
  • Tech Stack
  • Installation, File Structure, Using the App
  • Testing Approach
  • Test Data Management & Using Test Data
  • Custom Commands
  • Responsive Testing
  • Continuous Integration
  • Code Coverage

@ceceliacreates

Resources

@ceceliacreates

Q&A

Exploring the Cypress Real World App

By Cecelia Martinez

Exploring the Cypress Real World App

Cypress UK Community Meetup, 7/15/20

  • 303
Loading comments...

More from Cecelia Martinez