[React Testing] React Testing library note
User-event:
https://testing-library.com/docs/ecosystem-user-event/
Jest-dom:
https://github.com/testing-library/jest-dom#with-typescript
jest-axe
https://github.com/nickcolley/jest-axe
getBy* vs queryBy*
getBy*: if not found, throw error, sometime you don't want test fail because no element found.
queryBy*: if not found, return null, so you can do .toBeNull()
Mock
import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {reportError as mockReportError} from '../api'
import {ErrorBoundary} from '../error-boundary'
jest.mock('../api')
beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => {})
})
afterAll(() => {
console.error.mockRestore()
})
afterEach(() => {
jest.clearAllMocks()
})
function Bomb({shouldThrow}) {
if (shouldThrow) {
throw new Error('💣')
} else {
return null
}
}
test('calls reportError and renders that there was a problem', async () => {
mockReportError.mockResolvedValueOnce({success: true})
const {rerender} = render(<Bomb />, {wrapper: ErrorBoundary})
rerender(<Bomb shouldThrow={true} />)
const error = expect.any(Error)
const info = {componentStack: expect.stringContaining('Bomb')}
expect(mockReportError).toHaveBeenCalledWith(error, info)
expect(mockReportError).toHaveBeenCalledTimes(1)
expect(console.error).toHaveBeenCalledTimes(2)
expect(screen.getByRole('alert').textContent).toMatchInlineSnapshot(
`"There was a problem."`,
)
console.error.mockClear()
mockReportError.mockClear()
rerender(<Bomb />)
await userEvent.click(screen.getByText(/try again/i))
expect(mockReportError).not.toHaveBeenCalled()
expect(console.error).not.toHaveBeenCalled()
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
expect(screen.queryByText(/try again/i)).not.toBeInTheDocument()
})
Mock a component from 3rd-party library, skip 1000ms transition, make test run faster
import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {HiddenMessage} from '../hidden-message'
jest.mock('react-transition-group', () => {
return {
CSSTransition: (props) => (props.in ? props.children : null),
}
})
test('shows hidden message when toggle is clicked', async () => {
const myMessage = 'hello world'
render(<HiddenMessage>{myMessage}</HiddenMessage>)
const toggleButton = screen.getByText(/toggle/i)
expect(screen.queryByText(myMessage)).not.toBeInTheDocument()
await userEvent.click(toggleButton)
expect(screen.getByText(myMessage)).toBeInTheDocument()
await userEvent.click(toggleButton)
expect(screen.queryByText(myMessage)).not.toBeInTheDocument()
})
http jest mock:
import React from 'react'
import {render, screen, waitFor} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {loadGreeting as mockLoadGreeting} from '../api'
import {GreetingLoader} from '../greeting-loader-01-mocking'
jest.mock('../api')
test('loads greetings on click', async () => {
const testGreeting = 'TEST_GREETING'
mockLoadGreeting.mockResolvedValueOnce({data: {greeting: testGreeting}})
render(<GreetingLoader />)
const nameInput = screen.getByLabelText(/name/i)
const loadButton = screen.getByText(/load/i)
await userEvent.type(nameInput, 'Mary')
await userEvent.click(loadButton)
expect(mockLoadGreeting).toHaveBeenCalledWith('Mary')
expect(mockLoadGreeting).toHaveBeenCalledTimes(1)
await waitFor(() =>
expect(screen.getByLabelText(/greeting/i)).toHaveTextContent(testGreeting),
)
})
msw for mock http server
npm i whatwg-fetch
npm i msw
import 'whatwg-fetch'
import * as React from 'react'
import {render, screen, waitFor} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {rest} from 'msw'
import {setupServer} from 'msw/node'
import {GreetingLoader} from '../greeting-loader-01-mocking'
const server = setupServer(
rest.post('/greeting', (req, res, ctx) => {
return res(ctx.json({data: {greeting: `Hello ${req.body.subject}`}}))
}),
)
beforeAll(() => server.listen({onUnhandledRequest: 'error'}))
afterAll(() => server.close())
afterEach(() => server.resetHandlers())
test('loads greetings on click', async () => {
render(<GreetingLoader />)
const nameInput = screen.getByLabelText(/name/i)
const loadButton = screen.getByText(/load/i)
userEvent.type(nameInput, 'Mary')
userEvent.click(loadButton)
await waitFor(() =>
expect(screen.getByLabelText(/greeting/i)).toHaveTextContent('Hello Mary'),
)
})
Test Unmounting a React Component with React Testing Library
Sometimes your react component may need to do some cleanup work in the return value from useEffect or useLayoutEffect, or the componentWillUnmount lifecycle method for class components. Luckily for us, as far as React Testing Library is concerned, whichever of those you use is an implementation detail, so your test looks exactly the same regardless of how you implement it. Check out how you can test that simply with React Testing Library and a <Countdown /> component.
import React from 'react'
import {render, act} from '@testing-library/react'
import {Countdown} from '../countdown'
beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => {})
})
afterAll(() => {
console.error.mockRestore()
})
afterEach(() => {
jest.clearAllMocks()
jest.useRealTimers()
})
test('does not attempt to set state when unmounted (to prevent memory leaks)', () => {
jest.useFakeTimers() // we don't want to wait 10 seconds
const {unmount} = render(<Countdown />)
unmount()
act(() => jest.runOnlyPendingTimers()) // Using act with useFakeTimers()
expect(console.error).not.toHaveBeenCalled()
})
Improve Reliability of Integration Tests using find* Queries from React Testing Library
By using some of the get queries, we’re assuming that those elements will be available on the page right when we execute the query. This is a bit of an implementation detail and it’d be cool if we could not make that assumption in our test. Let’s swap all those for find queries.
await userEvent.click(await findByText(/fill.*form/i))
await userEvent.click(await screen.findByText(/fill.*form/i))
Router testing
import React from 'react'
import {BrowserRouter} from 'react-router-dom'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {Main} from '../main'
test('main renders about and home and I can navigate to those pages', () => {
window.history.pushState({}, 'Test page', '/')
render(
<BrowserRouter>
<Main />
</BrowserRouter>,
)
expect(screen.getByRole('heading')).toHaveTextContent(/home/i)
userEvent.click(screen.getByText(/about/i))
expect(screen.getByRole('heading')).toHaveTextContent(/about/i)
})
test('landing on a bad page shows no match component', () => {
window.history.pushState({}, 'Test page', '/something-that-does-not-match')
render(
<BrowserRouter>
<Main />
</BrowserRouter>,
)
expect(screen.getByRole('heading')).toHaveTextContent(/404/i)
})
Write a Setup Function to Reduce Duplication of Testing Custom React Hooks
import React from 'react'
import {render, act} from '@testing-library/react'
import {useCounter} from '../use-counter'
function setup({initialProps} = {}) {
const result = {}
function TestComponent(props) {
result.current = useCounter(props)
return null
}
render(<TestComponent {...initialProps} />)
return result
}
test('exposes the count and increment/decrement functions', () => {
const result = setup()
expect(result.current.count).toBe(0)
act(() => result.current.increment())
expect(result.current.count).toBe(1)
act(() => result.current.decrement())
expect(result.current.count).toBe(0)
})
test('allows customization of the initial count', () => {
const result = setup({initialProps: {initialCount: 3}})
expect(result.current.count).toBe(3)
})
test('allows customization of the step', () => {
const result = setup({initialProps: {step: 2}})
expect(result.current.count).toBe(0)
act(() => result.current.increment())
expect(result.current.count).toBe(2)
act(() => result.current.decrement())
expect(result.current.count).toBe(0)
})
test('the step can be changed', () => {
const {result, rerender} = renderHook(useCounter, {
initialProps: {step: 3},
})
expect(result.current.count).toBe(0)
act(() => result.current.increment())
expect(result.current.count).toBe(3)
rerender({step: 2})
act(() => result.current.decrement())
expect(result.current.count).toBe(1)
})
Test React Portals with within from React Testing Library
When you use a React Portal, much of your component can be rendered outside the main DOM tree. Let’s see how we can use utilities provided by React Testing Library to allow us to select elements within the portal. To perform actions and assert on what’s rendered.
import React from 'react'
import {render, within} from '@testing-library/react'
import {Modal} from '../modal'
test('modal shows the children', () => {
render(
<Modal>
<div data-testid="test" />
</Modal>,
)
const {getByTestId} = within(document.getElementById('modal-root')) // within: only look for elements inside Modal
expect(getByTestId('test')).toBeInTheDocument()
})
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
2020-10-30 [Kotlin] Try resource, use {} block
2020-10-30 [Kotlin] Try catch & Throw
2020-10-30 [Kotlin] mutableList with toList
2018-10-30 [React] Use React.memo with a Function Component to get PureComponent Behavior
2018-10-30 [Javascript] Use a custom sort function on an Array in Javascript
2017-10-30 [Anuglar & NgRx] StoreRouterConnectingModule
2017-10-30 [Unit testing] data-test attr FTW