用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