使用 React Testing LIbrary 的 15 个常见错误

前言

哈喽,大家好,我是海怪。

刚开始我在写项目的单测方案的时候,老板就让我能够写一些单测的规范。虽然表面上我非常自然地说:没问题,但是心里还是慌得不行:以前我自己写单测也没啥规范呀,直接开干就好了。

最近一直在看 Kent 的文章,刚好看到他写的这篇 《Common mistakes with React Testing Library》,里面列举了很多别人写单测时经常犯的一些错误 。正好可以作为单测规范的参考。所以,今天就把这篇文章也分享给大家~

翻译中会尽量用更地道的语言,这也意味着会给原文加一层 Buf,想看原文的可点击 这里。


正片开始

哈喽,大家好。以前的我(Kent)并不是很喜欢那个时候的测试环境,为此写了一个 React Testing Library。它是原来 DOM Testing Library 的一个扩展,随着不断更新迭代,现在 Testing Library 的实现也能支持当下所有流行的 JS 框架和工具来定位组件中的 DOM 了。

随时代发展,我们也对这个库的 API 做了很多修改,同时也发现社区中有很多不怎么优雅的使用方式。虽然我们已经很努力地在文档里写要怎么 “更好地” 使用我们提供的工具 API,但我还是在别的文章和博客中看到他们在用这些不优雅的使用方法。接下来,我就一一盘点这些方法,解释为什么它们不是很好,以及如何改进测试以避免这些陷阱。

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

  • 低:一般为我的主观想法,如果你觉得使用上没啥问题可以忽略它
  • 中:如果你不遵循,可能会出现 Bugs、低效的测试用例、还可能会做额外的工作
  • 高:一定要用我建议的方法。不然很有可能你会遇到大问题,而且测试用例并不怎么高效

没有使用 Testing Library 的 ESLint 插件

重要程度:中

如果你想避免这些常见的错误,那么官方的 ESLint 插件可以给你带来很多帮助:

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

注:如果你已经在用 create-react-library,那 eslint-plugin-testing-library 已经包含包在依赖中了

建议:最好把这两个 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 的过时用法,现在已经不需要它了。而且 render 的返回值里也并没有 Wraper 任何东西,它只是一些工具 API 的集合而已。所以,一般情况下可以不需要它了。

建议:直接使用从 render 返回值解构出来的东西,或者将返回值命名为 view

手动使用 cleanup

重要性:中

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

afterEach(cleanup)

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

现在cleanup 都是自动调用的,所以你已经不再需要再考虑它了。详见这里。

建议:别手动调 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 引入的时候一并引入就可以了:

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

使用 screen 的好处是:在添加/删除 DOM Query 时,不需要实时地解构 render 的返回值来获取内容。输入 screen,你的编辑器就能自动补全它里面的 API 了。

除非一种情况:你在配置 container 或者 baseElement。不过,你应该避免使用它们(因为我实在想不出使用它们的现实场景,除非你是在处理一些历史遗留问题)。

你也可以直接调 screen.debug 而不是 debug

建议:用 screen 来做 Querying 和 Debugging

使用错误的断言 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:
//     

上面的 toBeDisabled 来自 jest-dom 这个库。强烈建议大家使用 jest-dom,因为你能获得更好的错误信息。

建议:用 @testing-library/jest-dom 这个库

将不必要的操作放在 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'})

我经常看到不少人像上面那样把一些操作放在 act 里,因为他们一看到 “act” 的 Warning,就把操作放在 act 里面,以此去掉 Warning。但他们不知道的是 renderfireEvent 已经包裹在 act 里了!所以这样么其实没啥卵用。

大多数时间,如果你看到这些 act 的 Warning,不是要让你无脑地干掉它们,是在告诉你:你的测试有问题了。可以看这里的视频来了解更多:Fix the “not wrapped in act(…)” warning。

建议:去了解什么时候应该用 act,别把啥东西都往 act 里放

使用错误的 Query

重要程度:高

// ❌
// 假设你有这样的 DOM:
// 
screen.getByTestId('username')

// ✅
// 改成通过关联 label 以及设置 type 来访问 DOM
// 
screen.getByRole('textbox', {name: /username/i})

我们文档里一直有维护一个页面:“Which query should I use?”。你应该按这个页面中的顺序来使用 Query API。如果你的目标和我们的一样,都想通过测试来确保用户在使用时应用能够正常工作的话,那你就要尽量用更接近用户的使用方式来查询 DOM。我们提供的 Query 都能帮你做到这一点,但并非所有 Query API 都是一样的。

使用 container 来查询元素

作为 “使用错误的 Query” 的子集,我想聊一下直接用 container 来查询元素的问题:

// ❌
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})

实际上我们更希望用户能直接和 UI 进行交互,然而,如果你用 querySelector 这些来做查询的话,不仅我们不能模仿用户的 UI 交互行为,测试代码也会变得很难读,而且容易崩。这和下面这一节也有关系:

没有用文本来做查询

作为 “使用错误的 Query” 的子集,我想聊一下为什么我们更建议你用真实的文本来做查询(关于地区语言,应该用默认的地区语言文本),而不是用 Test ID 以及别的一些机制。

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

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

如果不用真实的文本来查询,那你要做很多额外的工作,因为你要确保你的地区语言的翻译转换是正确的。这里肯定有多人会吐槽说:要是别人改了文本的内容,你的测试不就崩了么?我对此的反驳是,首先,如果有人将 “UserName” 更改为 “Email”,这是我绝对想知道的变更(因为我需要更改我的实现了)。而且,就算有人因为改了个名搞崩了测试,修复测试也用不了多长时间,马上就能修好了。

总的来说,修复的成本是很低的,而好处则是可以增加你对翻译正确性信心,而且写出来的测试也是容易阅读和修改的。

还是要声明一下,并不是所有人都同意我这个观点的,具体可以看下 Twitter 上的这个 Thread。

多数情况下没有使用 *ByRole

作为 “使用错误的 Query” 的子集,我想来聊聊 *ByRole。在最近 RTL 的几个版本里,对 *ByRole 相关的 Query API 都做了很多的升级,这了是对组件渲染输出做查询操作最推荐的方法。下面是我比较喜欢它的一些功能。

name 选项可以让你通过元素的 “Accessible Name” 查询元素,这也是 Screen Reader 会对每个元素读取的内容。好处是:即使元素的文本内容被其它不同元素分割了,它还是能够以此做查询。比如:

// 假如现在我们有这样的 DOM:
// 

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})
// ✅ 成功!

人们不使用 *ByRole 做查询的原因之一是他们不熟悉在元素上的隐式 Role。,没关系,大家可以参考 MDN,MDN 上有写这些元素上的 Role List。

另一个我喜欢这个 API 的功能是:如果不能通过指定好的 Role 找到元素,它不仅会像 get* 以及 find* API 一样把整个 DOM 树都打印出来,而且还会把当前能访问的 Role 都打印出来!

// 假设我们有这样的 DOM
// 
screen.getByRole('blah')

上面会报这样的错误:

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>

这里要注意的是,我们并没有为

你可能感兴趣的:(杂谈,webrtc,音视频,html5)