Jamie Chien

Jamie Chien • 2023-04-18

Unit Test (2):Query Functions 整理

範例

前言

Query Functions幫助我們找到要測試的 elements,但要把所有Query Functions記起來來尋找 elements 和 roles 是很困難的,因此這篇文章要用拆解的方式來幫助大家更簡單的記住這些 functions。也可以妥善利用工具來幫助找到 elements。這篇文章會先介紹一下工具,後面再來介紹這些Query Functions們~

目錄

以下會先介紹 Testing Playground 再介紹 Query Functions 們
不需要將這些 Query Functions 全部記起來,可以妥善利用 Testing Playground 工具

使用 Testing Playground 的方法有幾個:

方法 1

直接到 Testing Playground 的網站使用

方法 2

在測試的區塊裡面加上screen.logTestingPlaygroundURL()可以在跑測試的時候產生一串網址,點擊網址就可以導到有你 render 出來的 element 的 playground 去了~

Example

it('should render component correctly', () => {
  render(<Component />);

  screen.logTestingPlaygroundURL();

  // ...other logic

  // ...expect
});

接者npm run test {檔名}來跑測試,然後就會出現如下圖的一串網址

點擊網址就會直接導向 Testing Playground 了~

Query Functions

所有的 query functions 都是透過screen object 來使用的,這些 query functions 永遠都是從以下幾個名字開頭的: getBygetAllByqueryByqueryAllByfindByfindAllBy,並根據使用情境來加上結尾,所以我們可以得到一個公式:

Query Functions = 開頭 + 結尾

因此,以下將分成兩個部分,分別介紹 開頭結尾

開頭

Start of Function NameExamples
getBy...getByRole, getByText
getAllBy...getAllByText, getAllByDisplayValue
queryBy...queryByDisplayValue, queryByTitle
queryAllBy...queryAllByTitle, queryAllByText
findBy...findByRole, findByText
findAllBy...findAllByText, findAllByDisplayValue

Compare

Name0 matches1 matches>1 matchesAwait?Notes
getBy...ThrowElementThrowNO
getAllBy...ThrowElement[]Element[]NO
queryBy...nullElementThrowNO
queryAllBy...[]Element[]Element[]NO
findBy...ThrowElementThrowYES預設 1 秒內尋找 element
findAllBy...ThrowElement[]Element[]YES預設 1 秒內尋找 element

findBy, findAllBy 是在 1 秒內不斷 retry 尋找 element,1 秒後 findBy, findAllBy 沒有找到 element 或 findBy 找到多個 elements 時就會 reject 掉

findBygetBy*waitFor 的結合,他接收 waitFor options 來做為最後一個參數 (i.e. await screen.findByText(‘text’, queryOptions, waitForOptions) )

Use Scenario

Goal of testUse
證明 element 存在getBy, getAllBy
證明 element存在queryBy, queryAllBy
確保 element 最終存在findBy, findAllBy

結尾

將下面的結尾搭配上面提到的開頭(getBygetAllByqueryByqueryAllByfindByfindAllBy),就可以組成一個完整的 QueryFunction,詳細的使用方式可以點開下放結尾裡面的 example 來參考~~

…ByRole (Link) -> Recommended

透過 Aria Role 來找到elements

不建議自行新增aria role,可以使用implicit role搭配options (name, description) 來找到想找的element


How to Use

getByRole('{aria-role}', options) 其中的options有以下這些值可以選填:

options?: {
  hidden?: boolean = false,
  name?: TextMatch,
  description?: TextMatch,
  selected?: boolean,
  busy?: boolean,
  checked?: boolean,
  pressed?: boolean,
  suggest?: boolean,
  current?: boolean | string,
  expanded?: boolean,
  queryFallbacks?: boolean,
  level?: number,
  value?: {
    min?: number,
    max?: number,
    now?: number,
    text?: TextMatch,
  }
}
Example
UserList.js
  function ButtonComponent() {
    return (
      <div>
        <button>Submit</button>
        <button>Cancel</button>
      </div>
    )
  }
UserList.test.js
it('should render two buttons', () => {
  render(<ButtonComponent />);

  const submitBtn = screen.getByRole('button', { name: /submit/i });
  const cancelBtn = screen.getByRole('button', { name: /cancel/i });

  expect(submitBtn).toBeInTheDocument();
  expect(cancelBtn).toBeInTheDocument();
})

…ByText (Link)

透過包含的text來找到elements

Example
DataForm.js
  function DataForm() {
    return (
      <form>
        <h3>My Data Form</h3>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          value="jane@gmail.com"
        />
      </form>
    )
  }
DataForm.test.js
  it('should render element', () => {
    render(<DataForm />);

    const element = screen.getByText('My Data Form');

    expect(element).toBeInTheDocument();
  })

…ByLabelText (Link)

透過搭配的labels包含的text來找到elements

Example
DataForm.js
  function DataForm() {
    return (
      <form>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          value="jane@gmail.com"
        />
      </form>
    )
  }
DataForm.test.js
  it('should render element', () => {
    render(<DataForm />);

    const element = screen.getByLabelText('Email');

    expect(element).toBeInTheDocument();
  })

…ByPlaceholderText (Link)

透過placeholder text來找到elements

Example
DataForm.js
  function DataForm() {
    return (
      <form>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          value="jane@gmail.com"
          placeholder="input email"
        />
      </form>
    )
  }
DataForm.test.js
  it('should render element', () => {
    render(<DataForm />);

    const element = screen.getByPlaceholderText('input email');

    expect(element).toBeInTheDocument();
  })

…ByDisplayValue (Link)

透過當下的value來找到elements

Example
DataForm.js
  function DataForm() {
    return (
      <form>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          value="jane@gmail.com"
        />
      </form>
    )
  }
DataForm.test.js
  it('should render element', () => {
    render(<DataForm />);

    const element = screen.getByDisplayValue('jane@gmail.com');

    expect(element).toBeInTheDocument();
  })

…ByAltText (Link)

透過 alt attribute來找到elements

Example
DataForm.js
  function DataForm() {
    return (
      <form>
        <div data-testid="img-wrapper">
          <img src="myImg.jpg" alt="myImgAlt" />
        </div>
      </form>
    )
  }
DataForm.test.js
  it('should render element', () => {
    render(<DataForm />);

    const element = screen.getByAltText('myImgAlt');

    expect(element).toBeInTheDocument();
  })

…ByTitle (Link)

透過 title attribute來找到elements

Example
DataForm.js
  function DataForm() {
    return (
      <form>
        <button title="Click Me">
          Submit
        </button>
      </form>
    )
  }
DataForm.test.js
  it('should render element', () => {
    render(<DataForm />);

    const element = screen.getByTitle('Click Me');

    expect(element).toBeInTheDocument();
  })

…ByTestId (Link) -> NOT Recommended

透過 data-testid attribute來找到elements


How to Use

getByTestId('{data-testid}')

Example
DataForm.js
  function DataForm() {
    return (
      <form>
        <div data-testid="img-wrapper">
          <img src="myImg.jpg" alt="myImgAlt" />
        </div>
      </form>
    )
  }
DataForm.test.js
  it('should render element', () => {
    render(<DataForm />);

    const element = screen.getByTestId('img-wrapper');

    expect(element).toBeInTheDocument();
  })

以上皆以 getBy 來做範例

建議使用 ByRole 來做query function,除非 ByRole 比較難以做到才使用其他functions ( ByTestId 最不建議使用)

container

透過container可以取得你rendered的React Element的DOM node,因此可以透過呼叫 container.querySelector 來查看他的children

React Testing Library將會創建一個div,並將該div加到document

透過container.firstChild來取得rendered element的root element

避免使用container來query elements

Example
UserList.js
  <table>
    <thead>
      <tr>
        <th>Name</th>
        <th>Email</th>
      </tr>
    </thead>
    <tbody>
      {renderedUsers}
    </tbody>
  </table>
UserList.test.js
  it('should render row', () => {
    const users = [
      { name: 'jane', email: 'jane@gmail.com'},
      { name: 'sam', email: 'sam@gmail.com' }
    ];

    const { container } = render(<UserList users={users} />);

    const rows = container.querySelectorAll('thead th')

    expect(rows).toHaveLength(2);
  })

baseElement

你的React Element渲染到的DOM Node所在的container,如果沒有在render的選項中指定baseElement,那默認的值將會是document.body

當你想要測試的組件在 container div 之外渲染內容時就會很好用,例如:當你想要快snapshot試你的 portal 組件時,該組件直接在 body 中渲染它的 HTML

debug

這是console.log(prettyDOM(baseElement))的簡寫

建議使用 screen.debug

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

  const HelloWorld = () => <h1>Hello World</h1>
  const {debug} = render(<HelloWorld />)
  debug()
  // 以下是log出來的內容
  // <div>
  //   <h1>Hello World</h1>
  // </div>

rerender

如果你測試的component正在更新prop,可以使用rerender來確保component正確更新props

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

  const {rerender} = render(<NumberDisplay number={1} />)

  // re-render the same component with different props
  rerender(<NumberDisplay number={2} />)

unmount

可以讓rendered的component被unmount,當你測試的component被從畫面上移除時很有用

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

  const {container, unmount} = render(<Login />)
  unmount()
  // your component has been unmounted and now: container.innerHTML === ''

asFragment

從rendered的component回傳一個 DocumentFragment

cleanup

他會unmount那些透過 render mounted的React trees

act

為斷言準備一個component,包裹要渲染的code在裡面,並在調用 act() 時更新,這會使得測試更接近React在瀏覽器中的工作方式

Example
hook
  import { useState, useCallback } from 'react'

  export default function useCounter() {
    const [count, setCount] = useState(0)
    const increment = useCallback(() => setCount((x) => x + 1), [])
    return { count, increment }
  }
hook.test.js
  test('should increment counter', () => {
    const { result } = renderHook(() => useCounter())

    act(() => {
      result.current.increment()
    })

    expect(result.current.count).toBe(1)
  })

renderHook

渲染一個用於測試的component,这个component會調用包含的 hook 的callback。 直接看範例吧!!

Example
hook
  import { useState, useCallback } from 'react'

  export default function useCounter() {
    const [count, setCount] = useState(0)
    const increment = useCallback(() => setCount((x) => x + 1), [])
    return { count, increment }
  }
hook.test.js
  test('should use counter', () => {
    const { result } = renderHook(() => useCounter())

    expect(result.current.count).toBe(0)
    expect(typeof result.current.increment).toBe('function')
  })

Unit Test 系列其他文章

Unit Test (1):介紹單元測試
Unit Test (3):常見的 Matchers (Assertions) 整理