React Testing Library

這篇記錄 @testing-library/react 的一些學習筆記,主要來源於官網open in new window

Install

其他必須安裝的東西在另一篇中已紀錄,這邊不再多寫一次~

$ npm install --save-dev @testing-library/react @testing-library/jest-dom

Query

查找元素

三種方式

  • getBy: 尋找目標,不存在時噴錯
  • queryBy: 尋找目標,不存在時為 null
  • findBy: 等到目標到超時為止

推薦使用順序

  • getByLabelText
  • getByText
  • getByDisplayValue
  • getByTitle
  • getByTestId
import {screen, getByLabelText} from '@testing-library/dom'

// With screen:
const inputNode1 = screen.getByLabelText('Username')

// Without screen, you need to provide a container:
const container = document.querySelector('#app')
const inputNode2 = getByLabelText(container, 'Username')

Match

screen.getByText('Hello World') // full string match
screen.getByText('llo Worl', {exact: false}) // substring match
screen.getByText('hello world', {exact: false}) // ignore case

Debug

打印出像是 console.log(prettyDOM(element)) 的效果

prettyDOM 是一個內建的打印元素工具函數open in new window

// debug document
screen.debug()
// debug single element
screen.debug(screen.getByText('test'))
// debug multiple elements
screen.debug(screen.getAllByText('multi-test'))

Manual

const {container} = render(<MyComponent />)
const foo = container.querySelector('[data-foo="bar"]')

User Actions

fireEvent

透過 fireEvent 對指定元素進行事件觸發

import { render, screen, fireEvent } from '@testing-library/react'

const Button = ({onClick, children}) => (
  <button onClick={onClick}>{children}</button>
)

test('calls onClick prop when clicked', () => {
  const handleClick = jest.fn()
  render(<Button onClick={handleClick}>Click Me</Button>)
  fireEvent.click(screen.getByText(/click me/i))
  expect(handleClick).toHaveBeenCalledTimes(1)
})

或是推薦透過 @testing-library/user-event 套件來觸發 DOM 事件

使用時需注意,若 event 當中有 setTimeout 動作,必須在 setup 時加上 delay: null option,避免造成 timeout 問題,詳細討論可見這邊open in new window

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Dummy from './Dummy';

describe('test', () => {
  jest.useFakeTimers();
  const user = userEvent.setup({ delay: null });

  it('test event', () => {
    const onClick = jest.fn();
    const { getByRole } = render(<Dummy onClick={onClick} />);
    const button = getByRole('button');
    // 觸發事件
    await user.click(button);
    jest.runOnlyPendingTimers();
    expect(onClick).toHaveBeenCalledTimes(1);
  });
});

Async methods

findBy

實際上就是 getBy 查詢和 waitFor 的組合,當您期望一個元素出現但對 DOM 的更改可能不會立即發生時,請使用 findBy 查詢

const button = screen.getByRole('button', {name: 'Click Me'})
fireEvent.click(button)
await screen.findByText('Clicked once')
fireEvent.click(button)
await screen.findByText('Clicked twice')

waitFor

任何時候需要等待一段時間時,使用 waitFor

注意,waitFor 可能會多次運行回調,直到達到超時

// 等到 mockAPI 被執行一次後
await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1))

waitForElementToBeRemoved

waitForElementToBeRemoved 是以 waitFor 為基礎製作,用來等待從 DOM 中移除元素

const el = document.querySelector('div.getOuttaHere')
waitForElementToBeRemoved(document.querySelector('div.getOuttaHere')).then(() =>
  console.log('Element no longer in DOM'),
)
el.setAttribute('data-neat', true)
// other mutations are ignored...
el.parentElement.removeChild(el)
// logs 'Element no longer in DOM'

Appearance & Disappearance

$ npm install --save-dev @testing-library/jest-dom
test('movie title appears', async () => {
  // element is initially not present...
  // wait for appearance and return the element
  const movie = await findByText('the lion king')
})

test('movie title appears', async () => {
  // element is initially not present...

  // wait for appearance inside an assertion
  await waitFor(() => {
    expect(getByText('the lion king')).toBeInTheDocument()
  })
})
const submitButton = screen.queryByText('submit')
expect(submitButton).toBeNull() // it doesn't exist

const submitButtons = screen.queryAllByText('submit')
expect(submitButtons).toHaveLength(0) // expect no elements

Fake Timer

// Fake timers using Jest
beforeEach(() => {
  jest.useFakeTimers()
})

// Running all pending timers and switching to real timers using Jest
afterEach(() => {
  jest.runOnlyPendingTimers()
  jest.useRealTimers()
})

Querying Within Elements

使用 within 函數包裹元素,可直接在其內進行 querying,不用給予 container

import {render, within} from '@testing-library/react'

const {getByText} = render(<MyComponent />)
const messages = getByText('messages')
const helloMessage = within(messages).getByText('hello')

Configuration

import { configure } from '@testing-library/react'

configure({testIdAttribute: 'data-my-test-id'})

Redux Testing

Reducers

透過呼叫 reducer 我們可以給予相應的 state, action 對 store 進行測試操作

Redux in Components

撰寫一個客製化 render 函數如下

import { Provider as StoreProvider } from 'react-redux'
import { store } from '@/store'
import { render } from '@testing-library/react'

const AllTheProviders = ({children}) => {
  return (
    <StoreProvider store={store}>
      {children}
    </StoreProvider>
  )
}

const customRender = (
  ui,
  options
) => render(ui, {wrapper: AllTheProviders, ...options})

export * from '@testing-library/react'
export {customRender as render}
Last Updated:
Contributors: johnnywang