自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

使用 React Testing Library 的 15 個(gè)常見錯(cuò)誤

開發(fā) 前端
作為測(cè)試庫工具系列的維護(hù)者,我們盡最大努力使 API 能夠引導(dǎo)人們盡可能有效地使用,一些不足之處,我們會(huì)嘗試正確地記錄下來,即使這會(huì)非常地困難(尤其是 API 改動(dòng)/升級(jí)等)。

哈嘍,大家好。以前的我(Kent)并不是很喜歡那個(gè)時(shí)候的測(cè)試環(huán)境,為此寫了一個(gè) React Testing Library。它是原來 DOM Testing Library 的一個(gè)擴(kuò)展,隨著不斷更新迭代,現(xiàn)在 Testing Library 的實(shí)現(xiàn)也能支持當(dāng)下所有流行的 JS 框架和工具來定位組件中的 DOM 了。

隨時(shí)代發(fā)展,我們也對(duì)這個(gè)庫的 API 做了很多修改,同時(shí)也發(fā)現(xiàn)社區(qū)中有很多不怎么優(yōu)雅的使用方式。雖然我們已經(jīng)很努力地在文檔里寫要怎么 “更好地” 使用我們提供的工具 API,但我還是在別的文章和博客中看到他們?cè)谟眠@些不優(yōu)雅的使用方法。接下來,我就一一盤點(diǎn)這些方法,解釋為什么它們不是很好,以及如何改進(jìn)測(cè)試以避免這些陷阱。

注:下面是重要程度的說明。

低:一般為我的主觀想法,如果你覺得使用上沒啥問題可以忽略它

中:如果你不遵循,可能會(huì)出現(xiàn) Bugs、低效的測(cè)試用例、還可能會(huì)做額外的工作

高:一定要用我建議的方法。不然很有可能你會(huì)遇到大問題,而且測(cè)試用例并不怎么高效

沒有使用 Testing Library 的 ESLint 插件

重要程度:中

如果你想避免這些常見的錯(cuò)誤,那么官方的 ESLint 插件可以給你帶來很多幫助:

  • eslint-plugin-testing-library
  • eslint-plugin-jest-dom

注:如果你已經(jīng)在用 create-react-library,那 eslint-plugin-testing-library 已經(jīng)包含包在依賴中了

建議:最好把這兩個(gè) ESLint 插件都裝上。

還在用 Wrapper 作為 render 返回值的變量名

重要程度:低

// ?
const wrapper = render(<Example prop="1" />)
wrapper.rerender(<Example prop="2" />)

// ?
const {rerender} = render(<Example prop="1" />)
rerender(<Example prop="2" />)

Wrapper 是以前 Enzyme 的過時(shí)用法,現(xiàn)在已經(jīng)不需要它了。而且 render 的返回值里也并沒有 Wraper 任何東西,它只是一些工具 API 的集合而已。所以,一般情況下可以不需要它了。

建議:直接使用從 render 返回值解構(gòu)出來的東西,或者將返回值命名為 view。

手動(dòng)使用 cleanup

重要性:中

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

afterEach(cleanup)

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

現(xiàn)在cleanup 都是自動(dòng)調(diào)用的,所以你已經(jīng)不再需要再考慮它了。詳見這里。

建議:別手動(dòng)調(diào) cleanup

不用 screen

重要程度:中

// ?
const {getByRole} = render(<Example />)
const errorMessageNode = getByRole('alert')

// ?
render(<Example />)
const errorMessageNode = screen.getByRole('alert')

screen 是在 DOM Testing Library v6.11.0 引入的 (就就是說,你可以在 @testing-library/react@>=9 這些版本中使用它)。直接在 render 引入的時(shí)候一并引入就可以了:

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

使用 screen 的好處是:在添加/刪除 DOM Query 時(shí),不需要實(shí)時(shí)地解構(gòu) render 的返回值來獲取內(nèi)容。輸入 screen,你的編輯器就能自動(dòng)補(bǔ)全它里面的 API 了。

除非一種情況:你在配置 container 或者 baseElement。不過,你應(yīng)該避免使用它們(因?yàn)槲覍?shí)在想不出使用它們的現(xiàn)實(shí)場(chǎng)景,除非你是在處理一些歷史遺留問題)。

你也可以直接調(diào) screen.debug 而不是 debug。

建議:用 screen 來做 Querying 和 Debugging

使用錯(cuò)誤的斷言 API

重要程度:高

const button = screen.getByRole('button', {name: /disabled button/i})

// ?
expect(button.disabled).toBe(true)
// error message:
// expect(received).toBe(expected) // Object.is equality
//
// Expected: true
// Received: false

// ?
expect(button).toBeDisabled()
// error message:
// Received element is not disabled:
// <button />

建議:用 @testing-library/jest-dom 這個(gè)庫

將不必要的操作放在 act 里

重要程度:中

// ?
act(() => {
render(<Example />)
})

const input = screen.getByRole('textbox', {name: /choose a fruit/i})
act(() => {
fireEvent.keyDown(input, {key: 'ArrowDown'})
})

// ?
render(<Example />)
const input = screen.getByRole('textbox', {name: /choose a fruit/i})
fireEvent.keyDown(input, {key: 'ArrowDown'})

我經(jīng)??吹讲簧偃讼裆厦婺菢影岩恍┎僮鞣旁?act 里,因?yàn)樗麄円豢吹?"act" 的 Warning,就把操作放在 act 里面,以此去掉 Warning。但他們不知道的是 render 和 fireEvent 已經(jīng)包裹在 act 里了!所以這樣么其實(shí)沒啥卵用。

大多數(shù)時(shí)間,如果你看到這些 act 的 Warning,不是要讓你無腦地干掉它們,是在告訴你:你的測(cè)試有問題了。可以看這里的視頻來了解更多:Fix the "not wrapped in act(...)" warning。

建議:去了解什么時(shí)候應(yīng)該用 act,別把啥東西都往 act 里放

使用錯(cuò)誤的 Query

重要程度:高

// ?
// 假設(shè)你有這樣的 DOM:
// <label>Username</label><input data-testid="username" />
screen.getByTestId('username')

// ?
// 改成通過關(guān)聯(lián) label 以及設(shè)置 type 來訪問 DOM
// <label for="username">Username</label><input id="username" type="text" />
screen.getByRole('textbox', {name: /username/i})

我們文檔里一直有維護(hù)一個(gè)頁面:“Which query should I use?”。你應(yīng)該按這個(gè)頁面中的順序來使用 Query API。如果你的目標(biāo)和我們的一樣,都想通過測(cè)試來確保用戶在使用時(shí)應(yīng)用能夠正常工作的話,那你就要盡量用更接近用戶的使用方式來查詢 DOM。我們提供的 Query 都能幫你做到這一點(diǎn),但并非所有 Query API 都是一樣的。

使用 container 來查詢?cè)?/h4>

作為 “使用錯(cuò)誤的 Query” 的子集,我想聊一下直接用 container 來查詢?cè)氐膯栴}:

// ?
const {container} = render(<Example />)
const button = container.querySelector('.btn-primary')
expect(button).toHaveTextContent(/click me/i)

// ?
render(<Example />)
screen.getByRole('button', {name: /click me/i})

實(shí)際上我們更希望用戶能直接和 UI 進(jìn)行交互,然而,如果你用 querySelector 這些來做查詢的話,不僅我們不能模仿用戶的 UI 交互行為,測(cè)試代碼也會(huì)變得很難讀,而且容易崩。這和下面這一節(jié)也有關(guān)系:

沒有用文本來做查詢

作為 “使用錯(cuò)誤的 Query” 的子集,我想聊一下為什么我們更建議你用真實(shí)的文本來做查詢(關(guān)于地區(qū)語言,應(yīng)該用默認(rèn)的地區(qū)語言文本),而不是用 Test ID 以及別的一些機(jī)制。

// ?
screen.getByTestId('submit-button')

// ?
screen.getByRole('button', {name: /submit/i})

如果不用真實(shí)的文本來查詢,那你要做很多額外的工作,因?yàn)槟阋_保你的地區(qū)語言的翻譯轉(zhuǎn)換是正確的。這里肯定有多人會(huì)吐槽說:要是別人改了文本的內(nèi)容,你的測(cè)試不就崩了么?我對(duì)此的反駁是,首先,如果有人將 “UserName” 更改為 “Email”,這是我絕對(duì)想知道的變更(因?yàn)槲倚枰奈业膶?shí)現(xiàn)了)。而且,就算有人因?yàn)楦牧藗€(gè)名搞崩了測(cè)試,修復(fù)測(cè)試也用不了多長(zhǎng)時(shí)間,馬上就能修好了。

總的來說,修復(fù)的成本是很低的,而好處則是可以增加你對(duì)翻譯正確性信心,而且寫出來的測(cè)試也是容易閱讀和修改的。

還是要聲明一下,并不是所有人都同意我這個(gè)觀點(diǎn)的,具體可以看下 Twitter 上的這個(gè) Thread。

多數(shù)情況下沒有使用 *ByRole

作為 “使用錯(cuò)誤的 Query” 的子集,我想來聊聊 *ByRole。在最近 RTL 的幾個(gè)版本里,對(duì) *ByRole 相關(guān)的 Query API 都做了很多的升級(jí),這了是對(duì)組件渲染輸出做查詢操作最推薦的方法。下面是我比較喜歡它的一些功能。

name 選項(xiàng)可以讓你通過元素的 "Accessible Name" 查詢?cè)?,這也是 Screen Reader 會(huì)對(duì)每個(gè)元素讀取的內(nèi)容。好處是:即使元素的文本內(nèi)容被其它不同元素分割了,它還是能夠以此做查詢。比如:

// 假如現(xiàn)在我們有這樣的 DOM://

// 假如現(xiàn)在我們有這樣的 DOM:
// <button><span>Hello</span> <span>World</span></button>

screen.getByText(/hello world/i)
// ? 報(bào)錯(cuò):
// Unable to find an element with the text: /hello world/i. This could be
// because the text is broken up by multiple elements. In this case, you can
// provide a function for your text matcher to make your matcher more flexible.

screen.getByRole('button', {name: /hello world/i})
// ? 成功!

人們不使用 *ByRole 做查詢的原因之一是他們不熟悉在元素上的隱式 Role。,沒關(guān)系,大家可以參考 MDN,MDN 上有寫這些元素上的 Role List。

另一個(gè)我喜歡這個(gè) API 的功能是:如果不能通過指定好的 Role 找到元素,它不僅會(huì)像 get* 以及 find* API 一樣把整個(gè) DOM 樹都打印出來,而且還會(huì)把當(dāng)前能訪問的 Role 都打印出來!

// 假設(shè)我們有這樣的 DOM
// <button><span>Hello</span> <span>World</span></button>
screen.getByRole('blah')

上面會(huì)報(bào)這樣的錯(cuò)誤:

TestingLibraryElementError: Unable to find an accessible element with the role "blah"

Here are the accessible roles:

button:

Name "Hello World":
<button />

--------------------------------------------------

<body>
<div>
<button>
<span>
Hello
</span>

<span>
World
</span>
</button>
</div>
</body>

這里要注意的是,我們并沒有為設(shè)置 Role 而加上 role=button。因?yàn)檫@是隱式的 Role,下一節(jié)會(huì)詳細(xì)說明。

建議:閱讀并根據(jù) “Which Query Should I Use" Guide” 里的推薦順序來使用 Query

錯(cuò)誤地添加可訪問屬性:aria-,role

重要程度:高

// ?
render(<button role="button">Click me</button>)

// ?
render(<button>Click me</button>)

像上面那樣隨意添加/修改可訪問屬性(Accessibility Attributes)不僅沒有必要,而且還會(huì)把 Screen Reader 和用戶搞懵。只有當(dāng)無法滿足當(dāng)前的 HTML 語義時(shí)(比如你寫了一個(gè)非原生的 UI 組件,同時(shí)也要讓它 像 AutoComplete 一樣可訪問),你才應(yīng)該使用可訪問屬性。假如這就是你現(xiàn)在要開發(fā)的東西,那可以用現(xiàn)有的第三庫根據(jù) WAI-ARIA 實(shí)踐來實(shí)現(xiàn)可訪問性。它們一般會(huì)有一些 很好的樣例來參考。

注意:如果要讓 input 可以通過 role 來訪問,你需要指定對(duì)應(yīng)的 type 屬性值!

建議:避免錯(cuò)誤地添加不必要的或不正確的可訪問屬性

沒有使用 @testing-library/user-event

重要程度:高

// ?
fireEvent.change(input, {target: {value: 'hello world'}})

// ?
userEvent.type(input, 'hello world')

@testing-library/user-event 是在 fireEvent 基礎(chǔ)上實(shí)現(xiàn)的,但它提供了一些更接近用戶交互的方法。上面這個(gè)例子中,fireEvent.change 其實(shí)只觸發(fā)了 Input 的一個(gè) Change 事件。但是 type 則可以對(duì)每個(gè)字符都會(huì)觸發(fā) keyDown、keyPress 和 keyUp 一系列事件。這能更接近用戶的真實(shí)交互場(chǎng)景。好處是可以很好地和你當(dāng)前那些沒有監(jiān)聽 Change 事件的庫一起使用。

我們現(xiàn)在還在進(jìn)行 @testing-library/user-event 這個(gè)庫的開發(fā),來保證它能像它承諾的那樣:能夠觸發(fā)用戶在執(zhí)行特定操作時(shí)會(huì)觸發(fā)的所有相同事件。不過,現(xiàn)在它還沒完全做到這一點(diǎn),這也是為什么它還沒有合入 @testing-library/dom (可能在未來的某個(gè)時(shí)候會(huì)合入)。但是,我對(duì)它有足夠的信心,建議你多關(guān)注和使用它,而不是 fireEvent。

建議:盡可能地使用 @testing-library/user-event,而不是 fireEvent

沒有用 query* 來斷言元素不存在

重要程度:高

// ?
expect(screen.queryByRole('alert')).toBeInTheDocument()

// ?
expect(screen.getByRole('alert')).toBeInTheDocument()
expect(screen.queryByRole('alert')).not.toBeInTheDocument()

把暴露 query* 相關(guān)的 API 出來的唯一原因是:可以在找不到元素的情況下不會(huì)拋出異常(返回 null)。唯一的好處是可以用來判斷這個(gè)元素是否沒有被渲染到頁面上。這是很重要的,因?yàn)轭愃?get* 和 find* 相關(guān)的 API 在找不到元素時(shí)都會(huì)自動(dòng)拋出異常 —— 這樣你就可以看到渲染的內(nèi)容以及為什么找不到元素的原因。然而,query* 只會(huì)返回 null,所以 toBeInTheDocument 在這里最好的用法就是:判斷 null 不在 Document 上。

建議:query* API 只用于斷言當(dāng)前元素不能被找到

用 waitFor 等待 find* 的查詢結(jié)果

重要程度:高

// ?
const submitButton = await waitFor(() =>
screen.getByRole('button', {name: /submit/i}),
)

// ?
const submitButton = await screen.findByRole('button', {name: /submit/i})

上面兩段代碼幾乎是等價(jià)的(find* 其實(shí)也是在內(nèi)部用了 waitFor),但是第二種使用方法更清晰,而且拋出的錯(cuò)誤信息會(huì)更友好。

建議:當(dāng)查詢那些不能立馬能訪問到的元素時(shí),使用 find*

給 waitFor 傳空 callback

重要程度:高

// ?
await waitFor(() => {})
expect(window.fetch).toHaveBeenCalledWith('foo')
expect(window.fetch).toHaveBeenCalledTimes(1)

// ?
await waitFor(() => expect(window.fetch).toHaveBeenCalledWith('foo'))
expect(window.fetch).toHaveBeenCalledTimes(1)

waitFor 的目的是可以讓你等一些指定的事情發(fā)生。如果傳了空的 callback,可能它在今天還能 Work,因?yàn)槟阒皇窍朐?Event Loop 等一個(gè) Tick 就好了。但這樣你也會(huì)留下一個(gè)脆弱的測(cè)試用例,一旦改了某些異步邏輯它很可能就崩了。

建議:在 waitFor 里等待指定的斷言,不要傳空 callback

一個(gè) waitFor callback 里有多個(gè)斷言

重要程度:低

// ?
await waitFor(() => {
expect(window.fetch).toHaveBeenCalledWith('foo')
expect(window.fetch).toHaveBeenCalledTimes(1)
})

// ?
await waitFor(() => expect(window.fetch).toHaveBeenCalledWith('foo'))
expect(window.fetch).toHaveBeenCalledTimes(1)

在上面的例子中,如果 window.fetch 調(diào)用了兩次,那么 waitFor 就會(huì)失敗,但是我們就得等到超時(shí)了才能看到具體報(bào)錯(cuò)。而如果 waitFor 里只有一個(gè)斷言,我們則可以等待 UI 渲染到斷言的同時(shí),也可以在其中一個(gè)斷言失敗時(shí)更快地獲得報(bào)錯(cuò)信息。

建議:waitFor 的 callback 里只放一個(gè)斷言

在 waitFor 中使用副作用

重要程度:高

// ?
await waitFor(() => {
fireEvent.keyDown(input, {key: 'ArrowDown'})
expect(screen.getAllByRole('listitem')).toHaveLength(3)
})

// ?
fireEvent.keyDown(input, {key: 'ArrowDown'})
await waitFor(() => {
expect(screen.getAllByRole('listitem')).toHaveLength(3)
})

waitFor 適用的情況是:在執(zhí)行的操作和斷言之間存在不確定的時(shí)間量。因此,callback 可在不確定的時(shí)間和頻率(在間隔以及 DOM 變化時(shí)調(diào)用)被調(diào)用(或者檢查錯(cuò)誤)。所以這也意味著你的副作用可能會(huì)被多次調(diào)用!

同時(shí),這也意味著你不能在 waitFor 里面使用快照斷言(SnapShot Assertion)。如果你想要用快照斷言,首先要等待某些斷言走完了,然后再拍快照。

建議:把副作用放在 waitFor 回調(diào)的外面,回調(diào)里只能有斷言

用 get* 來做斷言

重要程度:低

// ?
screen.getByRole('alert', {name: /error/i})

// ?
expect(screen.getByRole('alert', {name: /error/i})).toBeInTheDocument()

雖然這不是什么大問題,但我還是想說下我的觀點(diǎn)。如果 get* API 找不到元素,它就會(huì)拋出異常,打印整個(gè) DOM 樹結(jié)構(gòu)(語法高亮),在 Debug 的時(shí)候很有用。也因?yàn)檫@點(diǎn),斷言是永遠(yuǎn)不可能失敗的(因?yàn)槿绻也坏皆?,查詢?cè)跀嘌灾皰伋霎惓?。

因?yàn)檫@個(gè)原因,很多人直接不做斷言了。這其實(shí)也還好,但是我個(gè)人通常來說,會(huì)把斷言留著,這樣可以讓后面做重構(gòu)、修改的人知道:這里不是個(gè)查詢操作,而是個(gè)斷言操作。

建議:如果你想斷言某個(gè)東西是否存在,那么就做顯式的斷言操作

總結(jié)

作為測(cè)試庫工具系列的維護(hù)者,我們盡最大努力使 API 能夠引導(dǎo)人們盡可能有效地使用,一些不足之處,我們會(huì)嘗試正確地記錄下來,即使這會(huì)非常地困難(尤其是 API 改動(dòng)/升級(jí)等)。希望這篇文章會(huì)幫到你,我們只是想你更有信心地交付你的代碼。

Good Luck!

好了,這篇外文就給大家?guī)У竭@里了。翻譯這篇文章還是花不少時(shí)間的,同時(shí)也學(xué)到了很多 RTL 這個(gè)庫的一些思想,希望大家也能吸收里面一些測(cè)試思路。

責(zé)任編輯:武曉燕 來源: 寫代碼的海怪
相關(guān)推薦

2025-04-28 04:22:00

技巧React代碼

2020-03-19 14:50:31

Reac單元測(cè)試前端

2022-10-10 09:00:35

ReactJSX組件

2020-05-29 14:30:35

Kubernetes開發(fā)錯(cuò)誤

2022-05-31 15:43:15

自動(dòng)化測(cè)試

2021-03-09 09:52:55

技術(shù)React Hooks'數(shù)據(jù)

2024-09-18 11:27:57

2024-03-21 15:01:44

2021-11-26 05:50:50

Promise JS項(xiàng)目

2021-06-16 15:04:06

JavaScript內(nèi)存開發(fā)

2019-10-14 16:39:50

云計(jì)算配置錯(cuò)誤企業(yè)

2020-03-20 15:10:09

Python錯(cuò)誤分析代碼

2021-12-30 21:51:10

JavaScript開發(fā)內(nèi)存

2018-01-12 14:57:06

React Nativ開發(fā)錯(cuò)誤

2015-07-29 10:46:20

Java錯(cuò)誤

2018-01-11 16:29:19

錯(cuò)誤HibernateJPQL

2009-08-27 11:12:04

C# foreach

2015-11-16 15:15:51

SaaS初創(chuàng)公司定價(jià)錯(cuò)誤

2025-02-10 00:00:00

技巧JavaStreams

2019-08-13 11:32:55

物聯(lián)網(wǎng)技術(shù)大數(shù)據(jù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)