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 :
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
}
}
|
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.
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.