Jest is a library for unit testing, tests of app units without external dependencies. Installation Jest for Next packages npm install --save-dev jest @testing-library/react @testing-library/jest-dom Configuration
// jest.config.js
const nextJest = require('next/jest')
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
})
// Add any custom config to be passed to Jest
const customJestConfig = {
// Add more setup options before each test is run
// setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
// if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
moduleDirectories: ['node_modules', '/'],
testEnvironment: 'jest-environment-jsdom',
}
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)
// package.json
"scripts": {
"dev": "node exportAllPostsCreate && next dev",
"build": "node exportAllPostsCreate && next build",
"start": "next start",
"lint": "next lint",
"test": "jest --watch"
},
// .eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true,
jest: true
},
extends: [
'plugin:react/recommended',
'standard'
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: [
'react',
'@typescript-eslint'
],
rules: {
'react/react-in-jsx-scope': 'off',
'space-before-function-paren': 'off',
'react/prop-types': 'off',
'import/no-absolute-path': 'off'
}
}
Run npm run test All assertions toHaveBeenCalledTimes , toHaveBeenCalledWith , toHaveBeenLastCalledWith , toHaveBeenNthCalledWith , toHaveClass , toHaveDisplayValue , toHaveErrorMessage , toHaveFocus , toHaveFormValues , toHaveLastReturnedWith , toHaveLength , toHaveNthReturnedWith , toHaveProperty , toHaveReturned , toHaveReturnedTimes , toHaveReturnedWith , toHaveStyle , toHaveTextContent , toHaveValue , toMatch , toMatchInlineSnapshot , toMatchObject , toMatchSnapshot , toReturn , toReturnTimes , toReturnWith , toStrictEqual , toThrow , toThrowError , toThrowErrorMatchingInlineSnapshot , toThrowErrorMatchingSnapshot , lastCalledWith , lastReturnedWith , not , nthCalledWith , nthReturnedWith , rejects , resolves , toBe , toBeCalled , toBeCalledTimes , toBeCalledWith , toBeChecked , toBeCloseTo , toBeDefined , toBeDisabled , toBeEmptyDOMElement , toBeEnabled , toBeFalsy , toBeGreaterThan , toBeGreaterThanOrEqual , toBeInTheDocument , toBeInstanceof , toBeInvalid , toBeLessThan , toBeLessThanOrEqual , toBeNaN , toBeNull , toBePartiallyChecked , toBeRequired , toBeTruthy , toBeUndefined , toBeValid , toBeVisible , toContain , toContainElement , toContainEqual , toContainHTML , toEqual , toHaveAccessibleDescription , toHaveAccessibleName , toHaveAttribute , toHaveBeenCalled , toHaveBeenCalledTimes , toHaveBeenCalledWith , toHaveBeenLastCalledWith Main assertions toBe Compares references in memory.
test('exact equality', () => {
expect(2 + 2).toBe(4) // true
})
not.toBe
test('exact equality for objects', () => {
expect({ a: 1 }).not.toBe({ a: 1 }) // false
})
toEqual Compares all values
test('values equality', () => {
expect({ a: 1 }).toEqual({ a: 1 }) // true
})
toMatchObject
test('partial equality', () => {
expect({ a: 1, b: 2 }).toMatchObject({ a: 1 })
})
toHaveProperty
test('toHaveProperty', () => {
expect({ a: 1, b: 2 }).toHaveProperty('a', 1)
})
toBeNull
test('null', () => {
const n = null
expect(n).toBeNull()
})
toBeDefined
test('null', () => {
const n = null
expect(n).toBeDefined()
})
toBeUndefined
test('null', () => {
const n = null
expect(n).not.toBeUndefined()
})
toBeTruthy
test('null', () => {
const n = null
expect(n).not.toBeTruthy()
})
toBeFalsy
test('null', () => {
const n = null
expect(n)toBeFalsy()
})
toBeGreaterThan
test('two plus two', () => {
const value = 2 + 2
expect(value).toBeGreaterThan(3)
})
toBeGreaterThanOrEqual
test('two plus two', () => {
const value = 2 + 2
expect(value).toBeGreaterThanOrEqual(3.5)
})
toBeLessThan
test('two plus two', () => {
const value = 2 + 2
expect(value).toBeLessThan(5)
})
toBeLessThanOrEqual
test('two plus two', () => {
const value = 2 + 2
expect(value).toBeLessThanOrEqual(4.5)
})
toBeCloseTo
test('adding floating point numbers', () => {
const value = 0.1 + 0.2
expect(value).toBeCloseTo(0.3)
})
toMatch
test('hi', () => {
expect('team').not.toMatch('hi')
})
test('there is no I in team', () => {
expect('team').not.toMatch(/I/)
})
test('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/)
})
toContain
test('arr contains', () => {
expect(['1', '2', '3', '4', '5']).toContain('2')
})
arrayContaining
test('arr contains', () => {
expect(['1', '2', '3', '4', '5']).toEqual(expect.arrayContaining(['1', '2']))
})
toThrow
function functionWithError() {
throw new Error('very bad error')
}
test('compiling android goes as expected', () => {
expect(() => functionWithError()).toThrow()
expect(() => functionWithError()).toThrow(Error)
expect(() => functionWithError()).toThrow('very bad error')
expect(() => functionWithError()).toThrow(/bad/)
})
Promises
test('get userId from json api', () => {
return axios('https://jsonplaceholder.typicode.com/posts/1')
.then(res => {
expect(res.data.userId).toBe(1)
})
})
test('get userId from json api with async await', async () => {
const res = await axios('https://jsonplaceholder.typicode.com/posts/1')
expect(res.data.userId).toBe(1)
})
resolves
test('promise resolves', async () => {
function promiseWithResolve() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('done'), 500)
})
}
await expect(promiseWithResolve()).resolves.toEqual('done')
})
rejects
test('promise rejects', async () => {
function promiseWithResolve() {
return new Promise((resolve, reject) => {
setTimeout(() => reject('error'), 500)
})
}
await expect(promiseWithResolve()).rejects.toEqual('error')
})
beforeEach & afterEach Calls function for every test in a file or for a describe block.
beforeEach(() => {
console.log('test starts')
})
afterEach(() => {
console.log('test ended')
})
beforeAll & afterAll Calls function before and after all tests in a file or for a describe block.
beforeAll(() => {
console.log('tests start')
})
afterAll(() => {
console.log('tests ended')
})
describe Group tests in describe block beforeEach , afterEach , beforeAll , afterAll inside describe block affect tests inside this block.
describe('test function name', () => {
beforeEach(() => {
console.log('starts before each function in describe block')
})
test('to be one', () => {
expect(1).toBe(1)
})
test('to be two', () => {
expect(2).toBe(2)
})
})
Mock function basics Read here .
test('mock function basics', () => {
const mockFn = jest.fn()
mockFn()
mockFn('arg1', 'arg2')
expect(mockFn).toBeCalled()
expect(mockFn).toBeCalledTimes(2)
expect(mockFn.mock.calls.length).toBe(2)
expect(mockFn).toBeCalledWith('arg1', 'arg2') // last call
console.log('mockFn.mock.calls', mockFn.mock.calls) // [ [], [ 'arg1', 'arg2' ] ]
expect(mockFn.mock.calls[1][0]).toBe('arg1')
expect(mockFn.mock.calls[1][1]).toBe('arg2')
})
afterAll(() => {
jest.clearAllMocks()
})
Mock function with return value
test('mock function with return value', () => {
const mockFn = jest.fn()
mockFn()
mockFn.mockReturnValue('hi')
expect(mockFn()).toBe('hi')
})
Mock function with resolve value
test('mock function with resolve value', async () => {
const mockFn = jest.fn()
mockFn()
mockFn.mockResolvedValue('hi')
expect(await mockFn()).toBe('hi')
console.log('mockFn.mock.results', mockFn.mock.results) // [ { type: 'return', value: undefined }, { type: 'return', value: 'hi' } ]
})
Mock with custom implementation
test('mock function with implementation', () => {
const mockFn = jest.fn()
mockFn.mockImplementation(arg => {
if (typeof arg === 'string') return arg
if (typeof arg === 'number') return 10 * arg
})
expect(mockFn('hi')).toBe('hi')
expect(mockFn(3)).toBe(30)
// shorthand
const mockFn2 = jest.fn(arg => 'hi')
expect(mockFn2('hi')).toBe('hi')
})
Mock function from other file
// foo-bar-baz.js
export const foo = 'foo'
export const bar = () => 'bar'
export default () => 'baz'
//test.js
import defaultExport, {bar, foo} from '../foo-bar-baz'
jest.mock('../foo-bar-baz', () => {
const originalModule = jest.requireActual('../foo-bar-baz')
//Mock the default export and named export 'foo'
return {
__esModule: true,
...originalModule,
default: jest.fn(() => 'mocked baz'),
foo: 'mocked foo',
}
})
test('should do a partial mock', () => {
const defaultExportResult = defaultExport()
expect(defaultExportResult).toBe('mocked baz')
expect(defaultExport).toHaveBeenCalled()
expect(foo).toBe('mocked foo')
expect(bar()).toBe('bar')
})
Mock Redux
jest.mock('react-redux', () => {
const originalModule = jest.requireActual('react-redux')
return {
__esModule: true,
...originalModule,
useSelector: jest.fn().mockReturnValue({
title: 'some title',
message: 'some message',
isOpen: true
}),
useDispatch: () => jest.fn()
}
})
spyOn Same as previous, but a bit different way.
test('spyon', async () => {
const obj = {
fetchPost: function () {
return axios('https://jsonplaceholder.typicode.com/posts/1')
.then(res => res.data)
}
}
console.log(await obj.fetchPost()) // { userId: 1, id: 1, title: 'sunt' }
// instead of calling real API we can spy on the function and replace its behavior
jest.spyOn(obj, 'fetchPost')
.mockImplementation(() => Promise.resolve('data is fetched, but it is not your business'))
console.log(await obj.fetchPost()) // data is fetched, but it is not your business
})
afterEach(() => {
jest.restoreAllMocks()
})
Mock function return different values on different calls
test('mock function return different values', () => {
const mockFn = jest
.fn()
.mockReturnValue('default')
.mockReturnValueOnce('hi')
.mockReturnValueOnce('bye')
expect(mockFn()).toBe('hi')
expect(mockFn()).toBe('bye')
expect(mockFn()).toBe('default')
})
Mock Mock the external library.
test('should mock the lib', () => {
jest.mock('shortid', () => {
return jest.fn(() => '23kDr6')
})
const id = require('shortid')
console.log(id()) // '23kDr6'
})
Run one test only temporarily change that test command to a test.only .
test.only('this will be the only test that runs', () => {
expect(true).toBe(false)
})
test('this test will not run', () => {
expect('A').toBe('A')
})
it Same as test , shorter and makes description sound like normal language.
it('should be 2', () => {
expect(1 + 1).toBe(2)
})
Suppress jest warning
describe('<Invoices />', () => {
it('should render the component', () => {
jest.spyOn(console, 'error').mockImplementation(() => {})
renderWithProvider(<Invoices />)
const invoices = screen.getByTestId('invoices')
expect(invoices).toBeInTheDocument()
})
})
Mock imported function I am testing a component which uses react-query functions to fetch a data on mount In test we do not want to fetch the data and need to mock it Component acts always the same no matter useUserQuery() returns So we need to mock it ones for all tests Component renders differently depending on useInvoicesQuery() return value Here we need to control the return value
export const InvoicesTable = () => {
const dispatch = useDispatch()
const { data: user } = useUserQuery()
const { data, isLoading } = useInvoicesQuery()
const isServiceCenter = selectIsServiceCenter(user)
return (
....
)
}
import { screen } from '@testing-library/react'
import { renderWithProvider } from 'testUtils/renderWithProvider'
import { getDefaultStore } from 'testUtils/defaultStore'
import { InvoicesTable } from './InvoicesTable'
import { useInvoicesQuery } from 'api/useInvoicesQuery'
const store = getDefaultStore()
store.query.searchInputValue = 'some search input value'
// returned value will be static in all test
jest.mock('api/useUserQuery', () => ({
useUserQuery: () => ({ data: { username: 'Jack Russell' } })
}))
// returned value can be controlled in different tests
jest.mock('api/useInvoicesQuery', () => ({
useInvoicesQuery: jest.fn()
}))
describe('<InvoicesTable />', () => {
it('should render tables content', () => {
useInvoicesQuery.mockReturnValue({ isLoading: false, data: {} })
renderWithProvider(<InvoicesTable />, {}, { preloadedState: store })
expect(screen.queryByTestId('spinner')).not.toBeInTheDocument()
expect(screen.getByText('14:27:21', { exact: false })).toBeInTheDocument()
})
})
describe('<InvoicesTable />', () => {
it('should show spinner', () => {
useInvoicesQuery.mockReturnValue({ isLoading: true, data: {} })
renderWithProvider(<InvoicesTable />, {}, { preloadedState: store })
expect(screen.queryByTestId('spinner')).toBeInTheDocument()
})
})