
Jamie Chien • 2023-04-18
Unit Test (2):Query Functions 整理

前言
Query Functions幫助我們找到要測試的 elements,但要把所有Query Functions記起來來尋找 elements 和 roles 是很困難的,因此這篇文章要用拆解的方式來幫助大家更簡單的記住這些 functions。也可以妥善利用工具來幫助找到 elements。這篇文章會先介紹一下工具,後面再來介紹這些Query Functions們~
目錄
Testing Playground (Link)
使用 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 永遠都是從以下幾個名字開頭的: getBy
、getAllBy
、queryBy
、queryAllBy
、findBy
、findAllBy
,並根據使用情境來加上結尾,所以我們可以得到一個公式:
Query Functions = 開頭 + 結尾
因此,以下將分成兩個部分,分別介紹 開頭 和 結尾
開頭
Start of Function Name | Examples |
---|---|
getBy... | getByRole, getByText |
getAllBy... | getAllByText, getAllByDisplayValue |
queryBy... | queryByDisplayValue, queryByTitle |
queryAllBy... | queryAllByTitle, queryAllByText |
findBy... | findByRole, findByText |
findAllBy... | findAllByText, findAllByDisplayValue |
Compare
Name | 0 matches | 1 matches | >1 matches | Await? | Notes |
---|---|---|---|---|---|
getBy... | Throw | Element | Throw | NO | |
getAllBy... | Throw | Element[] | Element[] | NO | |
queryBy... | null | Element | Throw | NO | |
queryAllBy... | [] | Element[] | Element[] | NO | |
findBy... | Throw | Element | Throw | YES | 預設 1 秒內尋找 element |
findAllBy... | Throw | Element[] | Element[] | YES | 預設 1 秒內尋找 element |
findBy
, findAllBy
是在 1 秒內不斷 retry 尋找 element,1 秒後
findBy
, findAllBy
沒有找到 element 或 findBy
找到多個
elements 時就會 reject 掉
findBy
是 getBy*
與
waitFor
的結合,他接收 waitFor
options 來做為最後一個參數 (i.e.
await screen.findByText(‘text’, queryOptions, waitForOptions)
)
Use Scenario
Goal of test | Use |
---|---|
證明 element 存在 | getBy, getAllBy |
證明 element不存在 | queryBy, queryAllBy |
確保 element 最終存在 | findBy, findAllBy |
結尾
將下面的結尾搭配上面提到的開頭(getBy
、getAllBy
、queryBy
、queryAllBy
、findBy
、findAllBy
),就可以組成一個完整的 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
function ButtonComponent() {
return (
<div>
<button>Submit</button>
<button>Cancel</button>
</div>
)
}
UserList.test.jsit('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
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
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
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
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
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
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
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
最不建議使用)
Other RTL Method (Link)
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
<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
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
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) 整理