React Testing Library使用心得

485 阅读6分钟

为什么需要单元测试

  • 保证重构的安全性:没有一成不变的代码,测试用例能给多变的代码结构一个定心丸,利于后期的重构和维护
  • 提高代码质量:写单元测试的过程本身就是一个code review的过程,在这个过程中,可以发现一些设计上的问题(比如代码设计的不可测试),代码编写方面的问题(比如一些边界条件的处理不当)等,做到及时发现及时修正,不需要等到测试阶段甚至上线之后再发现再修改
  • 帮助快速熟悉代码:单元测试不仅起到了测试的作用,还是一种很好的“文档”,通过单元测试,我们不需要深入的阅读代码,便能知道这段代码做什么工作,有哪些特殊情况需要考虑,包含哪些业务

React单元测试的逻辑

  • 分支渲染:测试的不同的分支渲染逻辑,例如根据传入的数组渲染元素时,传入空数组的时候、传入仅有一个数组元素的时候、传入多个的时候是否都能够按照需求正确渲染
  • 交互事件是否(以正确的参数)被调用:例如当某条产品被点击时,应该将产品相关的信息发送给埋点系统进行埋点;或某个按钮被点击后页面能否弹出所需弹窗

合格的测试

  • 快速性:单元测试可快速执行,支持频繁测试。
  • 独立性:单元测试不互相依赖,随时以任意顺序执行。
  • 可重复性: 单元测试可以反复执行且结果统一。
  • 表达清晰:看到测试时,就能知道它测的业务点是啥测试;挂掉时,能清楚地知道失败的业务场景、期望数据与实际输出的差异
    • 因此测试描述要语义清晰,且选用断言工具时,应注意除了要提供测试结果,还要能准确提供“期望值”与“实际值”的差异

注意事项

不需要手动调用cleanup

在支持afterEach的测试框架(如mocha, Jest, and Jasmine),会自动调用,无需手动调用

断言API的使用

推荐使用@testing-library/jest-dom库,测试结果的错误信息更清晰

github.com/testing-lib…

const button = screen.getByRole('button', {name: /submit/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 />

选择正确的Query

如果想通过测试来确保用户在使用时应用能够正常工作的话,那就要尽量用更接近用户的使用方式来查询 DOM

建议:阅读并根据testing-library.com/docs/querie…里的推荐顺序来使用Query

// 不推荐
// <label>Username</label><input data-testid="username" />
screen.getByTestId('username')


// 推荐
// <label for="username">Username</label><input id="username" type="text" />
screen.getByRole('textbox', {name: /username/i})

 

简单概括一下,推荐的使用优先级顺序:

  1. Queries Accessible to Everyone
    1. getByRole
      1. 注意:有些DOM元素自己就有默认的role,例如button(DOM元素默认的role请参考:www.w3.org/TR/html-ari…developer.mozilla.org/en-US/docs/…),所以设置与隐含aria语义相匹配的role和/或aria-*属性是不必要的,也不建议这样做,因为浏览器已经设置了这些属性,我们不能以与所描述的语义相冲突的方式使用role和aria-*属性。例如,按钮元素不能具有heading的role属性,因为按钮元素具有与heading角色冲突的默认特征
      2. 注意:如果要让 input 可以通过role来访问,需要指定对应的type属性值
      3. 优势之一:getByRole的第二个参数传入options以帮我们更准确地匹配到目标元素(options可选值请参考testing-library.com/docs/querie…)。例如,name选项通过元素的 "Accessible Name" 查询元素,这样即使元素的文本内容被其它不同元素分割了,它还是能够以此做查询。比如
// 假如现在我们有这样的DOM:
// <button><span>Hello</span><span>World</span></button>


screen.getByText(/hello world/i)
// 报错:
// 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})
    1. getByLabelText
    2. getByPlaceholderText
    3. getByText
    4. getByDisplayValue
  1. Semantic Queries
    1. getByAltText
    2. getByTitle
  2. Test IDs
    1. getByTestId
      1. 注意:优先级最低,只有在前面那些都无法匹配到元素的时候才建议使用

避免错误添加可访问属性例如aria-role

建议:尽可能避免手动添加这些可访问属性,除非语义HTML不能满足我们的用例需要(例如如果我们需要构建一个非原生的UI,希望让它具有像原生元素一样的可访问性)

// 错误用法
render(<button role="button">Click me</button>)
// 正确
render(<button>Click me</button>)

像上面那样随意添加/修改可访问属性(Accessibility Attributes)不仅没有必要,而且还会把 Screen Reader和用户搞懵

  • 特殊情况:例如我们给一个div传入的文本是 'The goal: to discover how the species adapted to the kind of heat that can melt\n目的是:发现该物种如何适应那种可以融化鞋子的高温',使用getByText也会报‘broken up by multiple elements’错误,而且div没有默认role,我们无法使用getByRole匹配到div。此时就可以给div添加一个role,但要符合语义(参考:www.w3.org/TR/html-ari…
    • role的规范:给div设置role的时候要按照规范,不能随便取名,否则eslint会报错 www.w3.org/TR/html-ari…

推荐使用userEvent

建议:在模拟用户交互事件的时候,优先使用userEvent而不是fireEvent

// 不推荐
fireEvent.change(input, {target: {value: 'hello world'}})
// 推荐
userEvent.type(input, 'hello world')

二者的异同:

  • 都是用来模拟用户事件的,比如点击事件等
  • fireEvent仅仅是触发了特定的事件,并将该事件分派到给定的DOM节点上。当你只是想测试当你的元素触发事件如被点击时会发生什么时,这在大多数情况下都能正常工作,但是当用户真正点击你的元素时,通常会有更多事件按顺序被触发
  • userEvent是在 fireEvent 基础上实现的,模拟了完整的交互,内部使用了fireEvent来触发一些事件,并模拟了真实用户交互触发它们的顺序
  • 上面这个例子中,fireEvent.change 其实只触发了 Input 的一个 Change 事件。但是userEvent.type则可以对每个字符都会触发 keyDown、keyPress 和 keyUp 一系列事件。这能更接近用户的真实交互场景。好处是可以很好地和你当前那些没有监听 Change 事件的库一起使用。
  • 再例如click事件,截取了一段源码

image.png

image.png userEvent源码:github.com/testing-lib…