testing-library 入门
安装依赖
bash1npm install @testing-library/jest-dom @testing-library/react @testing-library/user-event --save-dev
- @testing-library/jest-dom:用于 dom、样式类型等元素的选取
- @testing-library/react:用于对 react 代码测试
- @testing-library/user-event:用于模拟用户行为事件
安装完成后,需要将 jest-dom 引入全局环境,这里就使用setup文件来装载。
在 mock 文件夹内创建setup文件,并引入jest-dom
js1// setup.ts 2import '@testing-library/jest-dom'
jest-config.ts中设置jest-dom的引入路径
js1export default { 2 ... 3 setupFilesAfterEnv: ['./src/mock/setup.ts'], 4}
安装完后,测试一下是否支持testing-library。
js1// 注意修改测试文件后缀名为`tsx` 2import { render, screen } from '@testing-library/react' 3import App from './App' 4 5test('renders learn react link', () => { 6 render(<App />) 7 const linkElement = screen.getByText(/learn react/i) 8 expect(linkElement).toBeInTheDocument() 9})
render是用来渲染 react 组件的函数screen.getByText是查询器,这个查询器会根据文字来查询出页面中对应的 dom 节点toBeInTheDocument是 jest 提供的匹配器,表示 dom 节点能够正常渲染到页面上
如果编辑器是 vscode 的话,可以下载插件 Jest ——用来自动运行测试用例以及其他功能
chrome 浏览器中下载插件 Testing Playground。在测试组件时,经常需要用到查询器查询对应的 dom,查询器很多,比如 ByRole、ByText 等,用这个插件能够很好地提示我们用哪个查询器。——来自官方推荐
查看 dom 结构
将App.tsx中的代码替换成以下内容
js1import React from 'react' 2 3const title = 'Hello React' 4 5function App() { 6 return <div>{title}</div> 7} 8 9export default App
同时修改src/App.test.tsx
diff1import React from 'react'; 2import { render, screen } from '@testing-library/react'; 3 4import App from './App'; 5 6describe('App', () => { 7 test('renders App component', () => { 8 render(<App />); 9 10+ screen.debug(); 11 }); 12});
screen.debug方法能够打印出页面中现有的 dom 结构,通过它我们知道现在测试环境中的 dom 结构是这样的
html1<body> 2 <div> 3 <div>Hello React</div> 4 </div> 5</body>
接下来我们给 App 组件增加 useState 以及一个子组件让它看起来更像平时编写的组件
js1import React, { PropsWithChildren } from 'react' 2 3function App() { 4 const [search, setSearch] = React.useState('') 5 6 function handleChange(event: React.ChangeEvent<HTMLInputElement>) { 7 setSearch(event.target.value) 8 } 9 return ( 10 <div> 11 <Search value={search} onChange={handleChange}> 12 Search: 13 </Search> 14 15 <p>Searches for {search ? search : '...'}</p> 16 </div> 17 ) 18} 19interface SearchProps { 20 onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; 21 value: string; 22} 23const Search: React.FC<PropsWithChildren<SearchProps>> = ({ value, onChange, children }) => { 24 return ( 25 <div> 26 <label htmlFor="search">{children}</label> 27 <input id="search" type="text" value={value} onChange={onChange} /> 28 </div> 29 ) 30} 31 32export default App
新的 dom 结构如下:
html1<body> 2 <div> 3 <div> 4 <div> 5 <label for="search"> Search: </label> 6 <input id="search" type="text" value="" /> 7 </div> 8 <p>Searches for ...</p> 9 </div> 10 </div> 11</body>
getByText
testing-library提供了非常多的search function来抓取 dom 元素,抓取到的 dom 元素可以用来断言或者其他交互行为。
最简单的抓取:
diff1describe('App', () => { 2 test('renders App component', () => { 3 render(<App />); 4 5+ screen.getByText('Search:'); 6 }); 7});
可以始终使用
screen.debug来获取 dom 结构,这样方便查看 DOM 结构。screen 是屏幕对象,表示用户在屏幕上能够看到的内容的集合
抓取到 dom 后就可以用来做某些交互或者断言。
比如我们要断言是否存在 document 内
diff1describe('App', () => { 2 test('renders App component', () => { 3 render(<App />); 4 // 隐式断言,如果元素不存在则会抛出一个错误 5 screen.getByText('Search:'); 6 // 推荐使用显式断言 7+ expect(screen.getByText('Search:')).toBeInTheDocument(); 8 }); 9});
screen.getByText这样的 search 函数默认有隐式断言功能,如果被搜索的元素不在 document 中,就能够很快发现问题。
类似于 getByText 这样的函数特别多,除了挨个啃官方文档的 API 外,还有个简单的方法:
打开localhost:3000,利用 Testing Playground的功能能够很快知道想要搜索的 DOM 元素用什么 search 函数

Playground会给出一个比较不错的答案,是不是很方便呢?
getByRole
还有一个很常用的Search API —— getByRole。
这个 API 经常被用于抓取aria-role attributes(特性)的 HTML 标签。
ARIA (Accessible Rich Internet Applications) 是一组属性,用于定义使残障人士更容易访问 Web 内容和 Web 应用程序(尤其是使用 JavaScript 开发的应用程序)的方法。
除了开发者自己定义 aria-role外,部分 HTML 标签也会有隐形的aria-role来表示它的语义,getByRole能抓取到这些带有隐形 role 属性的标签。
比如,页面上的<Input/>就能够被抓取,使用插件试一下:

可以看到input的role属性为textbox,我们可以拷贝插件推荐的抓取代码来对抓取到的元素进行断言。
js1test('renders App component', () => { 2 render(<App />) 3 expect( 4 screen.getByRole('textbox', { 5 name: /search:/i, 6 }), 7 ).toBeInTheDocument() 8})
上面的代码用来断言label为search:,role 为textbox的元素是否存在于页面中。
除了input外,还有很多拥有默认的role属性的html 标签。
下面列举一些常用的 html 标签和它们隐藏的 role 属性:
| 标签 | 隐性 role |
|---|---|
| a , href | link |
| a (没有 href 属性), body, div, span | generic |
| form | form |
| h1 到 h6 | heading |
| html | document |
| img | img |
| p | paragraph |
| table | table |
| ul | list |
| li | listitem |
| input | textbox |
| button | button |
除了默认的 role 外,我们也可以自己加 role 对标签进行覆盖,比如下面的例子:
js1const CustomRoleDiv = () => <div role="__widget_container">widget</div>
js1expect(screen.getByRole('__widget_container')).toBeInTheDocument()
自定义的 role 会覆盖默认的 role 属性,大部分情况下,我们都应该用默认的 role 属性。
除了基础的角色 role 外,W3C 在 ARIA 语义的提案中还包含了 aria 属性,这个语义表明 role 语义的状态和属性,比如 “按压” 的 button, "隐藏" 的 button 等。
aria 属性用来表示对应角色下的额外特殊含义,可以帮助我们在多个相同角色中选中我们需要的那一个,它们在 W3C 提案中是这样含义的:
aria-hidden: 不在 DOM 树上访问的元素;aria-selected: 元素是否被选中;aria-checked: 元素是否被勾选;aria-current: 当前选中的元素;aria-pressed: 被按压的元素;aria-expanded:元素是否被展开;aria-level: 区域的等级,值得一提的是,h1 - h6 会有默认的aria-level属性,值对应 1-6;aria-describedby: 可以通过描述来定位额外的元素。
aria 属性并不会对显示造成影响,只是语义上的属性。
下面以 button 为例:
html1<button aria-pressed></button>
我们可以这么选到它:
js1screen.getByRole('button', { pressed: true })
可以看到getByRole的第二个参数可以用来描述 aria 的状态,以此选择到正确的元素。
使用aria-role搜索还有一个非常重要的知识点:
以下面的两个 div 元素为例:
html1 <div role="note" style={{ display: "none" }} aria-hidden> 2 1234 3 </div> 4 <div role="note">1234</div>
它们对应的抓取代码是这样的:
js1const [hiddenNote] = screen.getAllByRole('note', { hidden: true }) 2const normalNote = screen.getByRole('note')
有两个疑问 ❓:
- 两个元素的
role相同,为什么用getByRole直接可以抓取到第二个元素。 - 第一个
aria-hidden的元素为什么需要用getAllByRole。
第一个问题的答案是:
screen.getByRole("note")跟screen.getByRole("note",{ hidden : false })是一样的效果,所以可以直接忽略aria-hidden = true的元素。
第二个问题的答案是:
screen.getByRole("note",{ hidden : true })的意思是把aria-hidden = false的元素抓取过来(允许包含aria-hidden = true)的元素。
除了上面的七种状态筛选外,还有一个非常常用的 name 筛选器(Testing Playground 经常会用到它)。
这个筛选项可以查询可访问的名称,比如文本内容,或者**aria-label 属性**等。
例如以下 HTML 元素
html1<div aria-label="test_note">1234</div>
使用name筛选器可以抓取到它:
js1screen.getByRole('generic', { name: 'test_note' })
一般来说,我们只需要根据
Testing Playground给出的抓取代码就已经够用了,aria-相关的属性内容比较多,不需要强行记忆。
其他 Search API
getByText 和 getByRole是最常用的 search 页面 HTML 元素的函数。
还有一些针对不同的参照物提供的搜索函数(前缀加上getBy|queryBy|findBy):
-
LabelText :
html1<label for="search" /> -
PlaceholderText:
html1<input placeholder="Search" /> -
AltText:
html1<img alt="profile" /> -
DisplayValue:
html1<input value="JavaScript" /> -
Title:
html1<span title="title">123</span> -
TestId:
html1<div data-testid="custom-element" />testId比较特殊,它是通过data-testid自定义特性来指定一个后门查询方式。这个对整个页面的语义和功能来说是没有任何影响的,相当于只是我们单独加的一个标识来确定指定的区域,一般只有实在不知道怎么选取需要的区域,才会去使用它。
js1const testidItem = screen.getByTestId('custom-element')
findBy 和 queryBy
跟 getBy xxx相似的,还有另外的search变体 ——queryBy 和findBy
findBy 有以下搜索函数:
- findByText
- findByRole
- ...
searchBy 有以下搜索函数:
- queryByText
- queryByRole
- ...
可以看出来就是把前半段的 getBy 换成queryBy|findBy。
我们把testing-library的搜索 API 用 By隔开,分为前半段和后半段名称:
text、role、PlaceholderText等后半段的名称对应的是不同的参照物。queryBy|findBy|getBy这几个前半段的名称是用于对应不同的使用场景。
queryBy 和 getBy 的区别
我们已经知道了getBy会有隐式断言,所以假设我想要断言页面上没有某个元素时,用 getBy 是会报错的。
js1import React from 'react' 2import { render, screen } from '@testing-library/react' 3 4import App from './App' 5 6describe('App', () => { 7 test('renders App component', () => { 8 render(<App />) 9 10 screen.debug() 11 12 // fails 13 expect(screen.getByText(/Searches for JavaScript/)).toBeNull() 14 }) 15})
通过screen.debug,我们知道页面上是不存在内容为Searches for JavaScript的元素的,所以上面的代码会一直报错。我们没办法用 getBy 断言一个不存在的元素。
用 queryBy 换掉getBy就可以:
js1test('renders App component', () => { 2 render(<App />) 3 4 screen.debug() 5 6 // fails 7 expect(screen.queryByText(/Searches for JavaScript/)).toBeNull() 8})
findBy 和 getBy 的差别
findBy 用于最终将出现的异步元素。
举一个例子,App 组件在渲染后,将获取一次异步数据,然后异步数据获取完成后,会更新一次页面。
diff1import React, { PropsWithChildren } from 'react'; 2+ function getUser() { 3+ return Promise.resolve({ id: '1', name: 'Robin' }); 4+ } 5function App() { 6 const [search, setSearch] = React.useState(''); 7+ const [user, setUser] = React.useState<{ id: string; name: string } | null>(null); 8+ React.useEffect(() => { 9+ const loadUser = async () => { 10+ const user = await getUser(); 11+ setUser(user); 12+ }; 13 14+ loadUser(); 15+ }, []); 16 function handleChange(event: React.ChangeEvent<HTMLInputElement>) { 17 setSearch(event.target.value); 18 } 19 return ( 20 <div> 21+ {user ? <p>Signed in as {user.name}</p> : null} 22 23 <Search value={search} onChange={handleChange}> 24 Search: 25 </Search> 26 27 <p>Searches for {search ? search : '...'}</p> 28 </div> 29 ); 30}
上面的 App 组件在 useEffect执行前不会渲染<p>Signed in as {user.name}</p>,我们需要等它一会,findBy就具有 “等待” 的作用:
diff1describe('App', () => { 2 test('renders App component', async () => { 3 render(<App />); 4 5+ screen.debug(); 6+ expect(screen.queryByText(/Signed in as/)).toBeNull(); 7 8+ expect(await screen.findByText(/Signed in as/)).toBeInTheDocument(); 9+ screen.debug(); 10 }); 11});
如果仔细观察两个 screen.debug,会发现 dom 结构是不同的,在 await screen.findByText后面的debug打出来的 dom 中,会多一个
html1<p>Signed in as Robin</p>
findBy、queryBy、getBy 使用场景
如果你希望断言一个不存在的元素,则使用 queryBy
如果你希望等待一会后抓取到某个元素,则使用findBy
否则则使用默认的getBy
如何抓取多个元素
只需要在get/query/find单词后加上All
- getAllBy
- queryAllBy
- findAllBy
这些都会返回一个数组,里面保存着多个抓取到的元素。
断言函数
大部分的断言函数都来自于 Jest, testing-library也扩展了很多函数,这些函数可以在jest-dom查到。
根据使用场景可以分为页面是否可见、表单验证和代码验证等三类:
| 断言使用场景 | 断言 API |
|---|---|
| 页面可见 | toBeEmptyDOMElement toBeVisible toBeInTheDocument toHaveTextContent |
| 表单验证 | toBeDisabled toBeEnabled toBeRequired toHaveFocus toBeChecked toHaveFormValues toHaveValue |
| 代码验证 | toHaveAttribute toHaveClass toHaveStyle |
页面可见类
-
toBeEmptyDOMElement:标签之间是否有可见内容, 即使是空格也会失败;html1✅ 2<div aria-label="empty_note"></div> 3❌ 4<div aria-label="note">123</div> 5❌ 6<div aria-label="note"></div> -
toBeVisible:是否可见,从用户直接观察的角度看能否可见;html1❌ <div role="note" style={{ display: "none" }}>1234</div> 2❌ <div role="note" hidden={true}>1234</div> 3✅ <div role="note">1234</div> -
toBeInTheDocument:是否存在在文档中,document.body 是否存在这个元素。html1✅ <div role="note" hidden={true}>1234</div> 2✅ <div role="note">1234</div> 3✅ <div role="note" style={{ display: "none" }}>1234</div> -
toHaveTextContent:匹配对应节点有没有指定的内容html1<div role="note">1234</div>如果想匹配该元素的文本是否包含 1,则可以这么写
js1expect(screen.getByRole('note')).toHaveTextContent(/1/i)
表单验证类
- toBeDisabled:检查元素是否通过 disable 属性判断
- toBeEnabled:是否未被禁用,等同于
.not.toBeDisabled; - toBeRequired:元素是否必填
- toHaveFocus:元素是否聚焦
- toBeChecked:checkbox 或者是 radio 是否被选中
- toHaveFormValues:验证整体表单的值是否和预期值匹配
- toHaveValue:与
toHaveFormValues类似,不过不同的是toHaveValue验证某个单独的表单元素,而不是全部 - toHaveDisplayValue:断言
input、select、textarea是否有用户能看到的显示值
代码验证类
- toHaveAttribute:匹配元素是否具备某个特性
- toHaveClass:匹配元素是否包含某个 class
- toHaveStyle:匹配元素是否具有对应样式
- toContainElement:匹配一个元素是否包含另一个元素作为后代
- toContainHTML:断言表示 HTML 元素的字符串是否包含在另一个元素中。
fireEvent
我们可以使用fireEvent来模拟用户的行为。
js1describe('App', () => { 2 test('renders App component', async () => { 3 render(<App />) 4 5 screen.debug() 6 fireEvent.change( 7 screen.getByRole('textbox', { 8 name: /search:/i, 9 }), 10 { 11 target: { value: 'JavaScript' }, 12 }, 13 ) 14 screen.debug() 15 }) 16})
fireEvent.change 接受一个抓取到的元素和传递给 onChange函数的值为参数。
通过上面的代码,相当于用户模拟了在<input id="search" type="text" value={value} onChange={onChange} />这个组件上输入了JavaScript字符。
传给onChange 函数的参数是{target: { value: 'JavaScript' }}。对应源代码中的
js1function handleChange(event: React.ChangeEvent<HTMLInputElement>) { 2 setSearch(event.target.value) 3}
通过第二个screen.debug,能清楚看到input 的值被设置成了:
html1<input id="search" type="text" value="JavaScript" />
此外,还能看到RTL给我们报了一个 warning

这说明我们的组件有一些异步更新的操作。除了按照提示的使用act函数外,还能够用 findByText来消除这个警告:
diff1describe('App', () => { 2 test('renders App component', async () => { 3 render(<App />); 4 screen.debug(); 5+ expect(await screen.findByText(/Signed in as/i)).toBeInTheDocument(); 6 fireEvent.change(screen.getByRole('textbox', { name: /search:/i }), { 7 target: { value: 'JavaScript' }, 8 }); 9 10 screen.debug(); 11 }); 12});
接着我们再对 change 前和 change 后做一些断言。
diff1describe('App', () => { 2 test('renders App component', async () => { 3 render(<App />); 4 screen.debug(); 5 expect(await screen.findByText(/Signed in as/i)).toBeInTheDocument(); 6+ expect(screen.queryByText(/Searches for JavaScript/)).toBeNull(); 7 fireEvent.change(screen.getByRole('textbox', { name: /search:/i }), { 8 target: { value: 'JavaScript' }, 9 }); 10+ expect(screen.getByText(/Searches for JavaScript/)).toBeInTheDocument(); 11 12 screen.debug(); 13 }); 14});
我们使用queryBy搜索来检查元素在事件之前是否不存在,使用getBy搜索变体来检查它在事件之后是否存在。
getBy也可以替换成queryBy使用。
RTL 的 fireEvent可以直接模拟用户的行为,而且在之后可以用断言来判断此时应该发生什么。
userEvent
还有一个userEvent 扩展能够更真实地模拟用户的行为。
fireEvent.change可以模拟用户的输入行为。
userEvent.type也能做到,但是它还能触发 keyDown、keypress和 keyUp事件
diff1- fireEvent.change(screen.getByRole('textbox', { name: /search:/i }), { target: { value: 'JavaScript' } }); 2 3+ userEvent.type(screen.getByRole('textbox', { name: /search:/i }), 'JavaScript');
如果有可能的话,优先考虑 userEvent 再考虑fireEvent
mock callback 函数
有些时候我们需要进行单元测试,比如下面的子组件:
js1const Search: React.FC<PropsWithChildren<SearchProps>> = ({ value, onChange, children }) => { 2 return ( 3 <div> 4 <label htmlFor="search">{children}</label> 5 <input id="search" type="text" value={value} onChange={onChange} /> 6 </div> 7 ) 8}
我想要知道传递给 Search 组件的 onChange 方法是否被调用过。
这时候可以利用 jest.fn来模拟一个 callback,传递给 Search 组件并对 callback 的次数进行断言。
js1test('renders search component', async () => { 2 const callback = jest.fn() 3 render(<Search value="1" onChange={callback}></Search>) 4 5 userEvent.type(screen.getByRole('textbox'), 'JavaScript') 6 7 expect(callback).toBeCalledTimes(10) 8})
使用 userEvent.type 来模拟用户输入 JavaScript 时,共会触发 10 次按键。
如果使用 fireEvent 的话,只会触发一次。
js1 test('renders search component', async () => { 2 const callback = jest.fn(); 3 render(<Search value="1" onChange={callback}></Search>); 4 5 // userEvent.type(screen.getByRole('textbox'), 'JavaScript'); 6 fireEvent.change(screen.getByRole('textbox'), { 7 target: { value: 'JavaScript' }, 8 }); 9 10 // expect(callback).toBeCalledTimes(10); 11 expect(callback).toBeCalledTimes(1); 12 }); 13});
这也说明 userEvent 更接近用户行为。
mock axios
js1import React from 'react' 2import axios from 'axios' 3 4const URL = 'http://hn.algolia.com/api/v1/search' 5 6function App() { 7 const [stories, setStories] = React.useState([]) 8 const [error, setError] = React.useState(null) 9 10 async function handleFetch(event) { 11 let result 12 13 try { 14 result = await axios.get(`${URL}?query=React`) 15 16 setStories(result.data.hits) 17 } catch (error) { 18 setError(error) 19 } 20 } 21 22 return ( 23 <div> 24 <button type="button" onClick={handleFetch}> 25 Fetch Stories 26 </button> 27 28 {error && <span>Something went wrong ...</span>} 29 30 <ul> 31 {stories.map((story) => ( 32 <li key={story.objectID}> 33 <a href={story.url}>{story.title}</a> 34 </li> 35 ))} 36 </ul> 37 </div> 38 ) 39} 40 41export default App
当点击按钮后,就可以发送异步请求,并将结果渲染到页面上。我们不在乎 axios 请求到的结果是不是正确的,这是后端单元测试的内容。
我们需要自己手动 mock axios 来返回一个数据
js1import React from 'react' 2import axios from 'axios' 3import { render, screen } from '@testing-library/react' 4import userEvent from '@testing-library/user-event' 5 6import App from './App' 7 8jest.mock('axios') 9 10describe('App', () => { 11 test('fetches stories from an API and displays them', async () => { 12 const stories = [ 13 { objectID: '1', title: 'Hello' }, 14 { objectID: '2', title: 'React' }, 15 ] 16 17 axios.get.mockImplementationOnce(() => Promise.resolve({ data: { hits: stories } })) 18 19 render(<App />) 20 21 await userEvent.click(screen.getByRole('button')) 22 23 const items = await screen.findAllByRole('listitem') 24 25 expect(items).toHaveLength(2) 26 }) 27})
在模拟 API 并呈现组件之后,我们使用 userEvent API 单击按钮,该按钮将进入 API 请求。
由于请求是异步的,我们必须等待组件更新。与之前一样,我们使用RTL的findBy搜索来等待最终出现的元素。
还可以 mock 一个错误的数据,确保传送数据失败后,展示给用户的是一个带有 error 信息的页面
js1test('fetches stories from an API and fails', async () => { 2 axios.get.mockImplementationOnce(() => Promise.reject(new Error())) 3 4 render(<App />) 5 6 await userEvent.click(screen.getByRole('button')) 7 8 const message = await screen.findByText(/Something went wrong/) 9 10 expect(message).toBeInTheDocument() 11})
waitFor
findBy函数用来处理最终将出现的异步元素,它有一个默认的固定的时间——1000 毫秒,超过这个时间的异步元素将不会被 findBy查到。
参考这个例子:
js1function getUser() { 2 return new Promise<{ id: string; name: string }>((resolve, reject) => { 3 setTimeout(() => { 4 resolve({ id: '1', name: 'Robin' }) 5 }, 1000) 6 }) 7} 8const Async = () => { 9 const [search, setSearch] = React.useState('') 10 const [user, setUser] = React.useState<{ id: string; name: string } | null>(null) 11 React.useEffect(() => { 12 const loadUser = async () => { 13 const user = await getUser() 14 setUser(user) 15 } 16 17 loadUser() 18 }, []) 19 return <div>{user ? <p>Signed in as {user.name}</p> : <p>loading</p>}</div> 20} 21 22export default Async
以下是用findBy来查询元素的代码:
js1it('async component', async () => { 2 render(<Async />) 3 expect(await screen.findByText(/Signed in as/)).toBeInTheDocument() 4})
最终这段代码会报错,原因是findByText并没有查到内容。
如果把定时器的时间改成 900,test代码最终能成功运行。
js1function getUser() { 2 return new Promise<{ id: string; name: string }>((resolve, reject) => { 3 setTimeout(() => { 4 resolve({ id: '1', name: 'Robin' }) 5 }, 900) 6 }) 7}
有没有一种方法可以取代findBy,让@testing-library能够等得更久一点?
答案是waitFor函数,findBy相当于waitFor和getBy的语法糖,用waitFor可以自定义等待的时间,所以方法更加灵活。
它是这么写的:
js1it('async component', async () => { 2 render(<Async />) 3 await waitFor( 4 () => { 5 screen.debug() // 通过 debug 查看 dom 结构 6 expect(screen.getByText(/Signed in as/)).toBeInTheDocument() 7 }, 8 { 9 timeout: 1100, // 比异步时间长一点点 10 interval: 100, // 执行间隔 11 }, 12 ) 13})
waitfor接收两个参数
-
第一个是需要重复执行的回调函数,我们可以在其中查询元素并且断言,
waitfor会根据设定(或者默认)的超时时间和执行间隔来重复执行回调。 -
第二个参数是可以配置的数据,比如说超时时间(timeout)、执行间隔(interval),通过这个参数我们就可以自定义我们需要的超时场景。
renderHook
当项目有一些公用的 hooks 时,可以用 renderHook来测试它的功能,对于能改变 state 的行为,我们需要放在 act 函数中做一次更新。
以下是hooks示例:
js1import { useCallback, useState } from 'react' 2 3const useCount = () => { 4 const [num, setNum] = useState(0) 5 6 const increase = useCallback(() => { 7 setNum(num + 1) 8 }, [num]) 9 10 return { num, increase } 11} 12 13export default useCount
对它的测试代码示例:
js1import { act, renderHook } from '@testing-library/react' 2it('test react hook', () => { 3 const { result } = renderHook(() => useCount()) 4 console.log(result) 5 expect(result.current.num).toBe(0) 6 act(() => { 7 /* fire events that update state */ 8 result.current.increase() 9 }) 10 expect(result.current.num).toBe(1) 11})
renderHook会将useCount的结果放在 result属性中,我们对其解构后从result.current中取出hook返回的内容,再对其进行断言即可。
触发 state 的修改的操作要放在act中以触发rerender。
组件 snapshot
何时适用
快照测试和它的字面意思一样,通过“快速(简单)拍出的照片”来测试,它是将我们需要判定的内容存储下来,在下一次匹配时,会判断两次的结果能否一致,从而达到从整体维度保证组件 UI 完整的能力。
我们可以把组件的快照测试输出的内容理解为 screen.debug(),比对的目的只是需要保证前后两次 debug 的输出是相同的。
基于这个特点,在以下几种情况中,我们可以使用 snapshot 来替代断言测试:
- 每个版本迭代后,都需要测试组件 UI 的完整性
- 某些不常更新的配置,确保它没有被随意修改
- 不轻易改变,甚至不会去改变的一些公共逻辑
使用快照
React Testing library 中提供了组件快照测试的能力,我们只需要使用它提供给我们的 toMatchSnapshot 断言就好。
以下是示例:
js1it('snapshot a component', () => { 2 const res = render(<AriaButton />) 3 expect(res.asFragment()).toMatchSnapshot() 4})
随后当前测试的组件快照会被保存在名为__snapshots__的文件夹内,它的格式长这样:
js1exports[`初始化测试 snapshot a component 1`] = ` 2<DocumentFragment> 3 <button 4 aria-describedby="hello" 5 > 6 <div 7 id="hello" 8 > 9 AriaButton 10 </div> 11 <div> 12 123 13 </div> 14 </button> 15</DocumentFragment> 16`
当然,我们也可以仅快照组件一部分
js1it('snapshot a part of component', () => { 2 render( 3 <div> 4 <button>click here</button> 5 </div>, 6 ) 7 expect(screen.getAllByRole('button', { name: 'click here' })).toMatchSnapshot() 8})
同样会生成一份 snapshot 数据:
js1exports[`初始化测试 snapshot a part of component 1`] = ` 2[ 3 <button> 4 click here 5 </button>, 6] 7`
当下次执行这个测试用例时,Jest 会将当前处理的结果与生成的 snapshot 数据对比,如果发生变化就会抛错。
我们随手改一下代码
diff1const AriaButton = () => { 2 return ( 3 <button aria-describedby="hello"> 4 <div id="hello">AriaButton</div> 5- <div>123</div> 6 </button> 7 ) 8}
运行 Jest 测试代码后 Jest 会抛出两次对比失败的结果:
js1 Snapshot name: `初始化测试 snapshot a component 1` 2 3 - Snapshot - 3 4 + Received + 0 5 6 @@ -5,10 +5,7 @@ 7 <div 8 id="hello" 9 > 10 AriaButton 11 </div> 12 - <div> 13 - 123 14 - </div> 15 </button> 16 </DocumentFragment>
更新快照
快照测试失败有两种原因:
- 代码误修改
- 确实发生了某些我们允许的变化
当第 2 种情况时,我们就需要更新快照测试中的结果,在 vscode 中右键后点击update snapshot即可。

当然,也可以使用命令行的形式,详情请查阅更新快照。
行内快照
有一些 DOM 少的组件或者选项少的 configuration,我们并不希望它生成文件,那么可以使用toMatchInlineSnapshot,它会在测试函数内直接生成一段字符串。
js1it('snapshot inline', () => { 2 const configuration = { env: 'dev', host: 'localhost' } 3 expect(configuration).toMatchInlineSnapshot(` 4{ 5 "env": "dev", 6 "host": "localhost", 7} 8`) 9})
上面这段字符串是调用 it 函数后,Jest 自动加上去的。
行内快照看起来跟断言一样,它适用于被快照的对象数据量足够小的情况。
如果快照出来的数据很多,使用它会影响测试代码的可读性。