Skip to content

Instantly share code, notes, and snippets.

@ostens
Last active January 10, 2020 11:41
Show Gist options
  • Save ostens/b94e93fde7580c797ed9f401ccaa5784 to your computer and use it in GitHub Desktop.
Save ostens/b94e93fde7580c797ed9f401ccaa5784 to your computer and use it in GitHub Desktop.
Testing and setting up with Cypress

Cypress

We added cypress E2E testing to our grad project. We found it really user-friendly, straightforward to set-up, and easy to read and write tests. Here's a run through of how we set it up and some things we learned about best practices.

Contents

We installed cypress using: npm install cypress --save-dev in the client directory of our project.

We then ran: npx cypress open to start up cypress. This opened the test runner UI which we used to choose which tests to run and inspect the results. On install, cypress helpfully creates example tests and default configuration files so before writing any code we were able to start getting our heads around how it worked.

A cypress.json file was automatically created in the client folder, which is where the default cypress configuration was specified. The rest of the basic configuration setup and examples were stored in a cypress folder which cypress creates for you.

As we were using multiple E2E testing frameworks on the project, we wanted to store these files in a directory below our e2e directory, so we modified cypress.json to:

  • Set integrationFolder to e2e/cypress/tests (where we'd be writing our tests)
  • Set pluginsFile to false (we wouldn't be using plugins)
  • Set fixturesFolder to false (we wouldn't be loading data from separate files)
  • Set supportFile to false (we wouldn't use the same setup across all our different tests)
  • Set baseUrl to http://localhost

We also added a script to package.json so that we could run cypress using an npm script:

"test:cypress": cypress open"

Our project used eslint so we also needed to install the cypress plugin for eslint using:

npm i --save-dev eslint-plugin-cypress

and update the eslint config .eslintrc.json by adding:

plugin:cypress/recommended to the extends array and cypress to the plugins array.

Cypress best practices came in really useful when we were writing tests and setting up the structure of our test flow. One thing this stresses is that it's really bad practice to use the UI to do things like logging in to test restricted pages/pages that require authentication.

Doing this is slow and repetitive but most importantly, specs should be tested in isolation. Logging in for tests that aren't explicity testing the login function should be done programmatically. This is good practice for all tests, not just cypress.

So, what did we do instead? If you don't know much about authentication, in different apps it can be set via cookies, localStorage or sessionStorage, but in our app we authenticated users using jwts stored in sessionStorage.

If you're not sure, you can check in devTools, under the Application menu and Storage.

The format of our authorization storage object was:

Authorization: Bearer string
Username: String
UserEmail: String
UserRole: Role type

Our helper file therefore looked a bit like:

const setSessionStorage = user => {
  cy.visit("/", { //visit homepage (relative to baseurl)
    onBeforeLoad(win) { //intercept the page before the UI loads to set credentials
      const userCredentials = [
        { Authorization: "Bearer jwt" },
        { UserName: "Fake name" },
        { UserEmail: "Fake email" },
        { UserRole: "AUTHORIZED_ROLE" }
      ];
      userCredentials.map(userCredential => {
        win.sessionStorage.setItem(
          Object.keys(userCredential),
          Object.values(userCredential)
        );
      });
    }
  });
};

We then ran this function before accessing restricted pages rather than using the UI to login each time, meaning we were testing just the target functionality. We did also have an explicit login test - but this was the only place logging in was done through the UI. Success!

Our app's workflow for publishing reports was a bit like:

User A submits a report (POST) => User B gets User A's report (GET) => User B approves user A's report (POST) => User A & B can see User A's report on their feeds (GET)

While all these actions are done by a user on the UI, we'd learned that doing the report submission on the UI to test the viewing of a report on the feed would be very out of scope, making the tests slower, much more likely to be flaky and much harder to troubleshoot.

So instead, we set up API helper functions to post and approve reports, so that when testing the display of a published report, that would be all that was being tested.

They looked a bit like this:

const approveReport = (reportId, jwt) => {
  cy.request({
    method: "POST",
    url: `/api/reviews/${reportId}/approve`,
    headers: {
      "Content-Type": "application/json",
      authorization: jwt
    }
  });
};

Approving a report would also be tested by the UI - but only for the approving reports spec test!

To help minimise code and to be explicit about what we were actually testing in a test rather than what's setup, we made extensive use of beforeAll, beforeEach, afterAll and afterEach.

We used these for helper functions like createUser() (set up a user before all the tests start so that you can actually do stuff), setSessionStorage() (authenticate a session, again, so that you can access and do stuff that's restricted) and clearDb() (wipe the state after all the tests are completed).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment