Know this easily test React app
Phan Công Thắng
Posted on October 31, 2021
Jest and Testing Library were the most powerful tool for testing React App. In this post, we are going to discover the important concept of them.
Let's dig in!
This is the simplest test that we can write in the first time using Jest.
test('1 plus 2 equal 3', () => {
expect(1 + 2).toBe(3)
})
Test Asynchronous
Suppose that I have a fake API that returns the user response with id: 1
, in the test case, I intentionally set change id: 3
to check whether the test works properly or not, and I end up with a passed
message.
The reason is that the test case is completed before the promise finishes.
test('user is equal user in response', () => {
const user = {
userId: 1,
id: 3,
title: 'delectus aut autem',
completed: false,
}
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => response.json())
.then((json) => expect(user).toEqual(json))
})
In order to avoid this bug, we need to have return
in front of fetch
.
test('user is equal user in response', () => {
const user = {
userId: 1,
id: 3,
title: 'delectus aut autem',
completed: false,
}
return fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => response.json())
.then((json) => expect(user).toEqual(json))
})
The test case above can rewrite using async, await
:
test('user is equal user in response using async, await', async () => {
const user = {
userId: 1,
id: 2,
title: 'delectus aut autem',
completed: false,
}
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
const resJson = await res.json()
expect(user).toEqual(resJson)
})
Useful methods
beforeAll
: To add some code that we want to run once before the test cases is run.
afterAll
: To add some code that we want to run after all test cases are finished. e.g. clear the database.
beforeEach
: To add some code that we want to run before each test case.
afterEach
: To add some code that we want to run at the point that each test case finishes.
Suppose that I have three test cases, and I set:
beforeEach(() => {
console.log('beforeEach is working...')
})
Three console
will appear on my terminal. Conversely, Using beforeAll
I only see one console
.
The logic way is the same with afterEach
and afterAll
.
The order run
We already have describe
(combines many test cases), test
(test case).
What is the order that jest run if test file was mixed by many describe
, test
?
You only need to remember this order: describe
-> test
.
To illustrate:
describe('describe for demo', () => {
console.log('this is describe')
test('1 plus 2 equal 3', () => {
console.log('this is test case in describe')
expect(1 + 2).toBe(3)
})
describe('sub-describe for demo', () => {
console.log('this is sub-describe')
test('2 plus 2 equal 4', () => {
console.log('this is test case in sub-describe')
expect(2 + 2).toBe(4)
})
})
})
Can you spot on the order in the example above?
My terminal log:
this is describe
this is sub-describe
this is test case in describe
this is test case in sub-describe
Mock function
I think the most powerful of Jest is having a mock function that we are able to mock the params
, object
which defined by the new
keyword, and customize the return value.
This is an example:
function plusTwoNumbers(
list: Array<number>,
callback: (a: number, b: number) => void,
) {
callback(list[0], list[1])
}
test('mock function callback', () => {
const mockFnc = jest.fn((a, b) => console.log('total:', a + b))
plusTwoNumbers([1, 2], mockFnc)
})
We mock callback
function, get the params
of it, and customize the result console.log("total:", a + b)
.
We are also able to mock modules, e.g. I use uuid
in order to generate a unique id
.
When I move on to testing, instead of using uuid
, I can mock the uuid
module like the code below:
Normally, whenever I call uuid.v4()
I will get a random value like this: 5442486-0878-440c-9db1-a7006c25a39f
But I want my value to be 1234
, I can use the code below:
import * as uuid from 'uuid'
jest.mock('uuid')
test('mock uuid module', () => {
uuid.v4.mockReturnValue('1234')
console.log('uuid.v4()', uuid.v4())
// 1234
})
Otherwise, I can use mockImplementation
to customize.
uuid.v4.mockImplementation(() => '1234')
mockImplementation
is the function that we customize the function that is created from other modules.
Config Jest
I'm going to introduce to you about the most important configs in Jest.
Let's go!
collectCoverageFrom
This config helps Jest knows exactly the place that needs to collect information, and check coverage. It is very useful, you can run:
Run jest --coverage
in order to figure out the component, the function, we still need to write test, and discover the spots we still don't test yet.
moduleDirectories
This config points to the module
that we will use in the test
file.
By default, it was configured ["node_modules"]
, and we are able to use the the module under node_modules
folder in our test cases.
moduleNameMapper
This config provides for us the ability to access the resources, based on the place that we have set.
moduleNameMapper: {
"assets/(*)": [
"<rootDir>/images/$1"
]
}
See the example above, now we set the path assets/(*)
that pointed to <rootDir>/images/$1
.
If I set assets/logo.png
, Jest will find <rootDir>/images/logo.png
.
rootDir
By default, it is the place that contains jest.config.js
, package.json
.
The place is where Jest will find to use modules
, and run test cases.
It turns out I can set "rootDir: 'test'" and run test cases without config roots
, but I shouldn't do this.
roots
This is the config that we set the place that test files belong to.
For example:
If I set:
roots: ['pages/']
but I write test in __test__
folder which is the same level with pages/
. No test cases will be run with the config above. I need to change pages/
-> __test__
.
testMatch
We use this config in order to communicate to Jest what files we want to test, otherwise, please skip!
testPathIgnorePatterns
Please ignore files under a place, that is the reason this config exists.
transform
Sometimes, in our test cases, we write some new code that node
doesn't support at all, so we need to transform to the code that Jest can understand.
If my project use typescript
, I need to set up transform in order to make typescript
to javascript
code that node can understand.
transformIgnorePatterns
We might have some files, some folders we don't want to transform, so we use this config.
How to write test
We need to write tests in order to be more confident about the code that we wrote. So when we think about the test cases, the core concept is we have to think about the use case, do not think about the code. It means we must focus
into what's the future that the code can support for users.
This is the main concept when we think about creating test cases
.
e.g:
I have created a react-hook in order to support four features below:
returns the value in first data using first property, condition true.
returns the value in second data using second property, condition false.
returns the value in second data using first property, condition false.
returns the default value with second data undefined, condition false.
import * as React from 'react'
type Props<F, S> = {
condition: boolean
data: [F, S]
}
function useInitialState<F, S>({condition, data}: Props<F, S>) {
const giveMeState = React.useCallback(
(
property: keyof F,
anotherProperty: S extends undefined ? undefined : keyof S | undefined,
defaultValue: Array<string> | string | number | undefined,
) => {
return condition
? data[0][property]
: data[1]?.[anotherProperty ?? (property as unknown as keyof S)] ??
defaultValue
},
[condition, data],
)
return {giveMeState}
}
export {useInitialState}
So I only need to write four test cases for the four features above:
import {useInitialState} from '@/utils/hooks/initial-state'
import {renderHook} from '@testing-library/react-hooks'
describe('useInitialState', () => {
const mockFirstData = {
name: 'Thang',
age: '18',
}
test('returns the value in first data using first property, condition true', () => {
const mockSecondData = {
name: 'Phan',
age: 20,
}
const {result} = renderHook(() =>
useInitialState({
condition: Boolean(mockFirstData),
data: [mockFirstData, mockSecondData],
}),
)
const data = result.current.giveMeState('name', undefined, '')
expect(data).toBe(mockFirstData.name)
})
test('returns the value in second data using second property, condition false', () => {
const mockSecondData = {
firstName: 'Phan',
age: 20,
}
const {result} = renderHook(() =>
useInitialState({
condition: Boolean(false),
data: [mockFirstData, mockSecondData],
}),
)
const data = result.current.giveMeState('name', 'firstName', '')
expect(data).toBe(mockSecondData.firstName)
})
test('returns the value in second data using first property, condition false', () => {
const mockSecondData = {
name: 'Phan',
age: 20,
}
const {result} = renderHook(() =>
useInitialState({
condition: Boolean(false),
data: [mockFirstData, mockSecondData],
}),
)
const data = result.current.giveMeState('name', undefined, '')
expect(data).toBe(mockSecondData.name)
})
test('returns the default value with second data undefined, condition false', () => {
const mockDefaultValue = 21
const {result} = renderHook(() =>
useInitialState({
condition: Boolean(false),
data: [mockFirstData, undefined],
}),
)
const data = result.current.giveMeState('age', undefined, mockDefaultValue)
expect(data).toBe(mockDefaultValue)
})
})
Testing Library
Let's take a slight review about the main things in Testing Library.
- getBy..: we find the DOM element, throw error if no element is found.
- queryBy..: we find the DOM element, return null if no element is found.
- findBy..: we find the DOM element, throw an error if no element is found, the search process is a promise.
The list below is the priority we should use in order to write test nearer with the way that our app is used.
getByRole
getByLabelText
getByAltText
getByDisplayValue
For example:
I have a component that contains two components: AutoAddress
, Address
.I need to find the use case that I want to support in order to create test cases.
This is a test case: by default, name value of inputs was set
.
render the components
create the mockResult value
add assertions
test('by default, name of address input was set', async () => {
render(
<AutoAddress wasSubmitted={false}>
<Address wasSubmitted={false} />
</AutoAddress>,
)
const mockResult = {
namePrefectureSv: 'prefertureSv',
namePrefectureSvLabel: 'prefectureSvLabel',
nameCity: 'city',
}
expect(screen.getByLabelText('Prefecture Code')).toHaveAttribute(
'name',
mockResult.namePrefectureSv,
)
expect(screen.getByLabelText('Prefecture')).toHaveAttribute(
'name',
mockResult.namePrefectureSvLabel,
)
expect(screen.getByLabelText('City')).toHaveAttribute(
'name',
mockResult.nameCity,
)
})
And this is a test case: returns one address through postCode
.
render the components
create the mockResult value
mock the request API
input the postCode
click the search button
add assertions
test('returns one address through postCode', async () => {
const mockResult = [
{
id: '14109',
zipCode: '1880011',
prefectureCode: '13',
city: 'Tokyo',
},
]
server.use(
rest.get(
`${process.env.NEXT_PUBLIC_API_OFF_KINTO}/${API_ADDRESS}`,
(req, res, ctx) => {
return res(ctx.json(mockResult))
},
),
)
render(
<AutoAddress wasSubmitted={false}>
<Address wasSubmitted={false} />
</AutoAddress>,
)
// input the post code value
userEvent.type(screen.getByLabelText('first postCode'), '111')
userEvent.type(screen.getByLabelText('second postCode'), '1111')
// search the address
userEvent.click(screen.getByRole('button', {name: /search address/i}))
// wait for the search process finishes.
await waitForElementToBeRemoved(() =>
screen.getByRole('button', {name: /searching/i}),
)
const address = mockResult[0]
const {prefectureCode, city} = address
expect(screen.getByLabelText('Prefecture Code')).toHaveAttribute(
'value',
prefectureCode,
)
expect(screen.getByLabelText('Prefecture')).toHaveAttribute(
'value',
PREFECTURE_CODE[prefectureCode as keyof typeof PREFECTURE_CODE],
)
expect(screen.getByLabelText('City')).toHaveAttribute('value', city)
})
Recap
We just learned the main concepts in Testing React App! Let's recap some key points.
- Testing asynchronous need to have
return
in front ofpromise
. - We are able to control testing using Jest configs.
- Thinking test cases, we must forget about code, focus on the use case.
- The order of DOM methods in Testing Library.
Posted on October 31, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.