用testing-library编写react测试用例
testing-library 简介
什么是 testing-library
用于 DOM 和 UI 组件测试的一系列工具,主要 API 包含 DOM 查询, 更可以和其他测试工具配合,用于更多场景:
- 测试工具
- jest
- cypress
- 框架
- react
- vue
- svelte
UI 测试工具还有 Airbnb 的 enzyme,侧重有所不同:
- enzyme 用于保证 React 组件的输入输出结构
- testing-library 的特性
- 不面向具体组件代码进行测试
- 面向最终 DOM 进行测试(Query)
- 模拟用户的交互方式(
fireEvent
) - 所以也支持除了 React 以外的其他 UI 框架
为什么要用 testing-library
Writing Better Tests with React Testing Library - Time to React - August 2019
- 如果你需要 UI 测试
- 在 2019 年 JavaScript 明星项目 的测试分类中处于领先地位
create-react-app
已经使用@testing-library/react
,
以及 React 官方文档中也推荐使它用
学习 testing-library
学习路线
- 前置学习
- TypeScript
- Jest
- (React 测试)
- 学习 testing-library
- 实战
- 仿照文档中的 Recipe 章节进行练习
- 为业务中的 UI 组件编写测试
资料
自学教材
- 概览
- 通过写测试用例学习前端知识
12 分钟,通过编写测试学习其他前端知识 - Writing Better Tests with React Testing Library - Time to React - August 2019
16 分钟,介绍基本用法和理念,和 enzyme 的对比
- 通过写测试用例学习前端知识
- 官方文档
A11y 和 ARIA
testing-library 知识体系
package
@testing-library/dom
- 部分衍生库,可搭配使用
@testing-library/jest-dom
@testing-library/react
@testing-library/user-event
@testing-library/react-hooks
DOM API
是主要的 API,用于查找元素
- TextMatch 类型声明(query 查找参数)
- Matcher
- 字符串
- 正则
(content: string, element: HTMLElement) => boolean
- MatcherOptions
exact = true
:严格检查,false 时支持子字符串、不区分大小写trim = true
:首尾去空格collapseWhitespace = true
:去除全部多余空格normalizer
:自定义预处理函数
- Matcher
- Query 查询
- API 前缀
- Single(返回单个或报错)
getBy
findBy
:异步化(Promise)queryBy
- All(返回数组)
getAllBy
findAllBy
queryAllBy
- Single(返回单个或报错)
- API 后缀
- 主要
ByLabelText
:用于表单ByPlaceholderText
:用于表单ByText
:查询TextNode
ByDisplayValue
:输入框等当前值
- 语义
ByAltText
:img 的 alt 属性ByTitle
:title 属性或元素ByRole
:ARIA role
- 显式测试标签
ByTestId
:查找data-testid
属性
- 主要
screen
:用within
绑定了document.body
- API 前缀
fireEvent
(两种写法)fireEvent(element, new MouseEvent('click', options?))
fireEvent.click(element, options?)
wait
系列(Promise,轮询或响应式等待 dom 变更)wait
waitForElement
waitForDomChange
waitForElementToBeRemoved
- 其他
within
:包装 element 参数的函数getNodeText
:得到value
或textContent
getRoles
:将 HTML 根据 ARIA role 进行解析isInaccessible
:判断不可访问性,诸如aria-hidden="true"
prettyDOM
:HTML 格式化logRoles
:getRoles
的 log 版
configure
defaultHidden
:修改ByRole
的hidden
默认值testIdAttribute
:修改ByTestId
的data-testid
默认值
buildQueries
:封装自定义查询方法
和 jest-dom 一起
扩展 jest 的 expect
方法,新增了一些针对 dom 的断言函数
- API 列表
- 表单和输入
toBeDisabled
:判断属性(button
、input
、select
等)toBeEnabled
toBeInvalid
:根据aria-invalid
属性规则toBeValid
toBeRequired
:根据属性required
或aria-required
toBeChecked
:checkbox
、radio
toHaveValue
:checkbox
、radio
、select
toHaveFormValues
:表单当前数据
- 元素性质
toBeVisible
:可见性(综合判断)toBeInTheDocument
toHaveAttribute
toHaveClass
toHaveFocus
toHaveStyle
- 元素内容
toBeEmpty
:不包含任何内容(及空结构)toContainElement
toContainHTML
toHaveTextContent
- 表单和输入
和 user-event 一起
相比 fireEvent
,扩展了几个 API
- API 列表
click(element)
:单击dblClick(element)
:双击async type(element, text, [options])
:输入文本selectOptions(element, values)
:表单选择tab({shift, focusTrap})
:模拟 tab 键(切换 focus)
和 react 一起
@testing-library/react
== @testing-library/dom
+ 三个新 API
- API 列表
render
:基于了 ReactDOM 的render
,扩展了getBy
等方法cleanup
:清除内部的渲染树act
:包装了 React 的 act(保证渲染、事件全部完成以便执行后续测试)
testing-library 典型代码
参考 testing-library - Learn By Doing
Query 基本
// * ------------------------------------------------ Query Basic
test("Query Basic", () => {
const container = createHTML(
`<span> Hello World! </span>`,
);
// * ---------------- getBy
// getByText(dom, 'Hello'); // ❌ => Error, unable to find
getByText(container, "Hello World!"); // ✅ => HTMLSpanElement {}
getByText(container, /hello/i); // ✅
getByText(container, "Hello", { exact: false }); // ✅
// * MatcherFunction
getByText(container, (content, element) => {
return (
content.startsWith("Hello") &&
element.tagName.toLowerCase() === "span"
);
}); // ✅
// * ---------------- queryBy
queryByText(container, "Hello"); // ⭕ => null
queryByText(container, "Hello World!"); // ✅
// * ---------------- findBy (Promise)
findByText(container, /hello/i).then((e) => {
// console.log(prettyDOM(e));
}); // ✅ =>
// `<span>
// Hello World!
// </span>`
});
Query 部分 API
// * ------------------------------------------------ Query API
test("By***", () => {
const container = createHTML(`
<form>
<label for="username-input">Username</label>
<input id="username-input" />
</form>
`);
getByText(container, "Username"); // ✅ => HTMLLabelElement
getByLabelText(container, "Username"); // ✅ => HTMLInputElement
container.querySelector("input").value = "Learn Test";
getByDisplayValue(container, "Learn Test"); // ✅
});
test("ByTestId", () => {
const container = createHTML(`
<div>
<span data-testid='notThis'> Hello World! </span>
<span data-testid='target'> Hello World! </span>
</div>
`);
getByTestId(container, "target"); // ✅
});
// * ------------------------------------------------ within
test("within", () => {
const container = createHTML(
`<span> Hello World! </span>`,
);
const { getByText } = within(container);
getByText(/Hello/); // ✅
});
// * ------------------------------------------------ event
test("fireEvent", () => {
const container = createHTML(
`<button onClick="console.log('fire')"></button>`,
);
fireEvent(container, new MouseEvent("click"));
fireEvent.click(container);
});
wait 系列(异步)
// * ------------------------------------------------ wait
test("wait", async () => {
const container = createHTML(
`<span> Hello World! </span>`,
);
const asyncRender = (fn) => setTimeout(fn, 0);
asyncRender(() => (container.textContent = "Learn Test"));
await wait(() => getByText(container, "Learn Test"));
getByText(container, "Learn Test"); // ✅ => HTMLSpanElement
});
test("waitForElement", async () => {
const container = createHTML(`<div></div>`);
const asyncRender = (fn) => setTimeout(fn, 0);
asyncRender(() =>
container.appendChild(createHTML(`<span>Hello</span>`)),
);
const dom = await waitForElement(
() => getByText(container, "Hello"),
{ container },
); // ✅ => HTMLSpanElement
});
test("waitForDomChange", async () => {
const container = createHTML(`<div></div>`);
const asyncRender = (fn) => setTimeout(fn, 0);
asyncRender(() =>
container.appendChild(createHTML(`<span>Hello</span>`)),
);
await waitForDomChange({ container });
getByText(container, "Hello"); // ✅ => HTMLSpanElement
});
testing-library 相关
和 TypeScript 一起
安装 testing-library
系列库会自动安装 @types
声明文件,
以便更好地支持 TypeScript 自动完成功能
Make it so the TypeScript definitions work automatically without config #123
如 @testing-library/jest-dom
的依赖中包含 @types/testing-library__jest-dom