Hi ! This is a guide/cheatsheet that I comeback to read when I want to write tests for a project.

I thought this might help other fellow developers so here you go 😁

Setup

Install jest, cypress and helper libraries

1
yarn add jest @testing-library/react @testing-library/jest-dom -D

Config

In this section we’ll configure Jest and Cypress

Jest

Let’s create a config file for Jest in the root directory:

 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
module.exports = {
  // location.href will have this value
  testURL: "https://example.com",
  // Add here folders to ignore
  testPathIgnorePatterns: ["/node_modules/"],
  setupTestFrameworkScriptFile: require.resolve("./test/setup.js"),
  // path to components/modules to test
  modulePaths: ["<rootDir>/src"],
  moduleNameMapper: {
    // mock files that jest doesn't support like CSS and SVG files
    "\\.css$": "<rootDir>/test/module-mock.js",
    "\\.svg$": "<rootDir>/test/module-mock.js",
  },
  // collect coverage report from only the js files inside src
  collectCoverageFrom: ["**/src/**/*.js"],
  coverageThreshold: {
    global: {
      // 20 is just an example
      // you can change it to any value you want (below 100)
      statements: 20,
      branches: 20,
      functions: 20,
      lines: 20,
    },
  },
};

Now create a test folder in the root directory and create setup.js file inside it:

1
2
3
4
5
// cleanup helper
import "@testing-library/react/cleanup-after-each";
// custom matchers for jest
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom/extend-expect";

also create a module-mock.js in the same test folder :

1
module.exports = {};
Code coverage

In package.json add --coverage at the end of your test script:

1
2
3
4
5
6
7
{
  ...
  "scripts": {
    ...
    "test": "jest --coverage"
  }
}
Watch mode

When coding, use Jest in watch mode to get instant feedback about the tests related to the files you are changing. To use this feature, add a script to package.json and use it:

1
2
3
4
5
6
7
{
  ...
  "scripts": {
    ...
    "test:watch": "jest --watch"
  }
}

Cypress

Install cypress and helpers:

1
yarn add cypress @testing-library/cypress -D

then add a script to package.json to run cypress:

1
2
3
4
5
6
7
8
{
  ...
  "scripts": {
    ...
    "cy:open": "cypress open",
    "cy:run": "cypress run", // run all cypress tests
  }
}
1
yarn cy:open

Cypress records videos and takes screenshots of the app while running tests. Let’s add the folders that Cypress uses for this to .gitignore

1
2
3
  ...
  cypress/videos
  cypress/screenshots
cypress.json

When running cypress open for the first time, it creates a bunch of files and folders inside a folder in the root dir called cypress. It also creates a file in the root dir called cypress.json. That’s the configuration file cypress uses.

Let’s add a baseUrl to use in our E2E test:

1
2
3
4
//cypress.json
{
  "baseUrl": "http://localhost:3000"
}
@testing-library/cypress

@testing-library/cypress adds some very handy commands to cypress, let’s configure it:

Go to <rootDir>/cypress/support, open index.js and add this line:

1
2
import '@testing-library/cypress/add-commands'
...

Test utils (helpers):

Have a test-utils file that exports a set of tools that are used specifically for the project you are testing.

  • Example:

Export a render method that takes care of adding styled-components ThemeProvider HOC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import React from "react";
import { render as originalRender, wait } from "@testing-library/react";

const theme = {
  colors: {
    red: "red",
  },
};

function render(component, renderOptions) {
  const utils = originalRender(
    <ThemeProvider theme={theme}>{component}</ThemeProvider>,
    renderOptions
  );
  return {
    ...utils,
  };
}
export { render };

Now in your tests, import render from this test-utils file instead of @testing-library/react

Unit test

Write a unit test when you want to test the functionality of ONE function/component:

1
2
3
4
5
6
7
8
9
import React from "react";
import { render } from "@testing-library/react";
import Paragraph from "../paragraph";

test("renders the text given", () => {
  const { getByText } = render(<Paragraph>Hello</Paragraph>);

  expect(getByText(/Hello/i)).toBeInTheDocument();
});

Integration test

Write an integration test when you want to test the functionality of several components working togather:

 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
import React from "react";
import { MockedProvider } from "@apollo/react-testing";
import wait from "waait";
import { fireEvent } from "@testing-library/react";
import { render } from "../test-utils";
import App, { LOGIN_MUTATION } from "../app";

beforeEach(() => {
  window.localStorage.removeItem("token");
});

test("login as a user", async () => {
  const fakeUser = { id: 123, username: "fakeuser" };
  const fakeUserCredentials = {
    ...fakeUser,
    password: "stupidpassword123",
  };
  const token =
    "thisisjustanexampleofatoken-youcanuseafakedatageneratorinstead";
  const loginMutationMock = jest.fn();
  const loginMutationErrorMock = jest.fn();
  const mocks = [
    {
      request: {
        query: LOGIN_MUTATION,
        variables: {
          username: fakeUserCredentials.username,
          password: fakeUserCredentials.password,
        },
      },
      result: () => {
        loginMutationMock();
        return { data: { user: fakeUser, token: token } };
      },
      error: () => {
        loginMutationErrorMock();
      },
    },
  ];
  const { getByTestId, getByText, getByLabelText } = render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <App />
    </MockedProvider>
  );
  // open login form dialog/modal
  fireEvent.click(getByText(/login/i));
  // fill out login form
  const usernameNode = getByLabelText(/username/i);
  const passwordNode = getByLabelText(/password/i);
  usernameNode.value = fakeUserCredentials.username;
  passwordNode.value = fakeUserCredentials.password;
  // submit login form
  fireEvent.click(getByText(/sign in/i));
  // wait for the mocked requests to finish
  await wait(0);
  // assert calls
  expect(loginMutationMock).toHaveBeenCalledTimes(1);
  expect(loginMutationErrorMock).not.toHaveBeenCalled();
  // assert login side-effect
  expect(window.localStorage.getItem("token")).toBe(token);
  expect(getByTestId("username").textContent).toEqual(fakeUser.username);
});

End to end test:

Simplest definition : Imagine you’ve got a robot that obeys your commands, now ask it to test your app as a normal user 🤷‍♂️.

 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
describe("authentication and registration", () => {
  let user;

  beforeEach(() => {
    return cy
      .logout()
      .createNewUser()
      .then((u) => (user = u))
      .visit("/");
  });

  it("register as a guest user", () => {
    const user = {
      username: "user",
      email: "[email protected]",
      password: "password123",
    };
    cy.getByText(/register/i)
      .click()
      .getByLabelText(/username/i)
      .type(user.username)
      .getByLabelText(/email/i)
      .type(user.email)
      .getByLabelText(/password/i)
      .type(user.password)
      .getByText(/register/i)
      .click()
      .assertRoute("/");
    cy.getByTestId("username").should("contain", user.username);
  });

  it("login as a user", () => {
    cy.getByText(/login/i)
      .click()
      .getByLabelText(/username/i)
      .type(user.username)
      .getByLabelText(/password/i)
      .type(user.password)
      .getByText(/sign in/i)
      .click()
      .assertRoute("/");
    cy.getByTestId("username").should("contain", user.username);
  });
});

That’s it for this post. Checkout my newsletter 👇 if you want to get notify when I drop new posts. Peace.