NOTE: This is a cross-post from my newsletter and Blog. I publish each email one week after it’s sent. Subscribe to get more content like this earlier right in your inbox! 💌
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
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:
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:
// 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 :
module.exports = {}
Code coverage
In package.json add --coverage
at the end of your test
script:
{
...
"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:
{
...
"scripts": {
...
"test:watch": "jest --watch"
}
}
Cypress
Install cypress
and helpers:
yarn add cypress @testing-library/cypress -D
then add a script to package.json to run cypress:
{
...
"scripts": {
...
"cy:open": "cypress open",
"cy:run": "cypress run", // run all cypress tests
}
}
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
...
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:
//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:
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:
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:
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:
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 🤷♂️.
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: 'hello@example.com',
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
)
})
})
I'll try to improve this post and add to it but please feel free to send a PR in case you want to correct/add/edit something ❤️
Top comments (0)