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

更新日期: 2022-04-02阅读: 1k标签: 错误

哈喽,大家好。以前的我(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 返回值的变量名

重要程度:低

// :x:
const wrapper = render(<Example prop="1" />)
wrapper.rerender(<Example prop="2" />)

// :white_check_mark:
const {rerender} = render(<Example prop="1" />)
rerender(<Example prop="2" />)

Wrapper 是以前 Enzyme 的过时用法,现在已经不需要它了。而且 render 的返回值里也并没有 Wraper 任何东西,它只是一些工具 API 的集合而已。所以,一般情况下可以不需要它了。

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

手动使用 cleanup

重要性:中

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

afterEach(cleanup)

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

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

建议:别手动调 cleanup

不用 screen

重要程度:中

// :x:
const {getByRole} = render(<Example />)
const errorMessageNode = getByRole('alert')

// :white_check_mark:
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})

// :x:
expect(button.disabled).toBe(true)
// error message:
//  expect(received).toBe(expected) // Object.is equality
//
//  Expected: true
//  Received: false

// :white_check_mark:
expect(button).toBeDisabled()
// error message:
//   Received element is not disabled:
//     <button />

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

将不必要的操作放在 act 里

重要程度:中

// :x:
act(() => {
  render(<Example />)
})

const input = screen.getByRole('textbox', {name: /choose a fruit/i})
act(() => {
  fireEvent.keyDown(input, {key: 'ArrowDown'})
})

// :white_check_mark:
render(<Example />)
const input = screen.getByRole('textbox', {name: /choose a fruit/i})
fireEvent.keyDown(input, {key: 'ArrowDown'})

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

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

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

使用错误的 Query

重要程度:高

// :x:
// 假设你有这样的 DOM:
// <label>Username</label><input>"username" />
screen.getByTestId('username')

// :white_check_mark:
// 改成通过关联 label 以及设置 type 来访问 DOM
// <label for="username">Username</label><input id="username" type="text" />
screen.getByRole('textbox', {name: /username/i})

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

使用 container 来查询元素

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

// :x:
const {container} = render(<Example />)
const button = container.querySelector('.btn-primary')
expect(button).toHaveTextContent(/click me/i)

// :white_check_mark:
render(<Example />)
screen.getByRole('button', {name: /click me/i})

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

没有用文本来做查询

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

// :x:
screen.getByTestId('submit-button')

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

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

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

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

多数情况下没有使用 *ByRole

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

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

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

// 假如现在我们有这样的 DOM:
// <button><span>Hello</span> <span>World</span></button>

screen.getByText(/hello world/i)
// :x: 报错:
// 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})
// :white_check_mark: 成功!

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

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

// 假设我们有这样的 DOM
// <button><span>Hello</span> <span>World</span></button>
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>

这里要注意的是,我们并没有为设置 Role 而加上 role=button。因为这是隐式的 Role,下一节会详细说明。

建议:阅读并根据 “Which Query Should I Use" Guide” 里的推荐顺序来使用 Query

错误地添加可访问属性:aria-,role

重要程度:高

// :x:
render(<button role="button">Click me</button>)

// :white_check_mark:
render(<button>Click me</button>)

像上面那样随意添加/修改可访问属性(Accessibility Attributes)不仅没有必要,而且还会把 Screen Reader 和用户搞懵。只有当无法满足当前的 html 语义时(比如你写了一个非原生的 UI 组件,同时也要让它 像 AutoComplete 一样可访问),你才应该使用可访问属性。假如这就是你现在要开发的东西,那可以用现有的第三库根据 WAI-ARIA 实践来实现可访问性。它们一般会有一些 很好的样例来参考。

注意:如果要让 input 可以通过 role 来访问,你需要指定对应的 type 属性值!

建议:避免错误地添加不必要的或不正确的可访问属性

没有使用 @testing-library/user-event

重要程度:高

// :x:
fireEvent.change(input, {target: {value: 'hello world'}})

// :white_check_mark:
userEvent.type(input, 'hello world')

@testing-library/user-event 是在 fireEvent 基础上实现的,但它提供了一些更接近用户交互的方法。上面这个例子中,fireEvent.change 其实只触发了 Input 的一个 Change 事件。但是 type 则可以对每个字符都会触发 keyDown、keyPress 和 keyUp 一系列事件。这能更接近用户的真实交互场景。好处是可以很好地和你当前那些没有监听 Change 事件的库一起使用。

我们现在还在进行 @testing-library/user-event 这个库的开发,来保证它能像它承诺的那样:能够触发用户在执行特定操作时会触发的所有相同事件。不过,现在它还没完全做到这一点,这也是为什么它还没有合入 @testing-library/dom (可能在未来的某个时候会合入)。但是,我对它有足够的信心,建议你多关注和使用它,而不是 fireEvent。

建议:尽可能地使用 @testing-library/user-event,而不是 fireEvent

没有用 query* 来断言元素不存在

重要程度:高

// :x:
expect(screen.queryByRole('alert')).toBeInTheDocument()

// :white_check_mark:
expect(screen.getByRole('alert')).toBeInTheDocument()
expect(screen.queryByRole('alert')).not.toBeInTheDocument()

把暴露 query* 相关的 API 出来的唯一原因是:可以在找不到元素的情况下不会抛出异常(返回 null)。唯一的好处是可以用来判断这个元素是否没有被渲染到页面上。这是很重要的,因为类似 get* 和 find* 相关的 API 在找不到元素时都会自动抛出异常 —— 这样你就可以看到渲染的内容以及为什么找不到元素的原因。然而,query* 只会返回 null,所以 toBeInTheDocument 在这里最好的用法就是:判断 null 不在 Document 上。

建议:query* API 只用于断言当前元素不能被找到

用 waitFor 等待 find* 的查询结果

重要程度:高

// :x:
const submitButton = await waitFor(() =>
  screen.getByRole('button', {name: /submit/i}),
)

// :white_check_mark:
const submitButton = await screen.findByRole('button', {name: /submit/i})

上面两段代码几乎是等价的(find* 其实也是在内部用了 waitFor),但是第二种使用方法更清晰,而且抛出的错误信息会更友好。

建议:当查询那些不能立马能访问到的元素时,使用 find*

给 waitFor 传空 callback

重要程度:高

// :x:
await waitFor(() => {})
expect(window.fetch).toHaveBeenCalledWith('foo')
expect(window.fetch).toHaveBeenCalledTimes(1)

// :white_check_mark:
await waitFor(() => expect(window.fetch).toHaveBeenCalledWith('foo'))
expect(window.fetch).toHaveBeenCalledTimes(1)

waitFor 的目的是可以让你等一些指定的事情发生。如果传了空的 callback,可能它在今天还能 Work,因为你只是想在 Event Loop 等一个 Tick 就好了。但这样你也会留下一个脆弱的测试用例,一旦改了某些异步逻辑它很可能就崩了。

建议:在 waitFor 里等待指定的断言,不要传空 callback

一个 waitFor callback 里有多个断言

重要程度:低

// :x:
await waitFor(() => {
  expect(window.fetch).toHaveBeenCalledWith('foo')
  expect(window.fetch).toHaveBeenCalledTimes(1)
})

// :white_check_mark:
await waitFor(() => expect(window.fetch).toHaveBeenCalledWith('foo'))
expect(window.fetch).toHaveBeenCalledTimes(1)

在上面的例子中,如果 window.fetch 调用了两次,那么 waitFor 就会失败,但是我们就得等到超时了才能看到具体报错。而如果 waitFor 里只有一个断言,我们则可以等待 UI 渲染到断言的同时,也可以在其中一个断言失败时更快地获得报错信息。

建议:waitFor 的 callback 里只放一个断言

在 waitFor 中使用副作用

重要程度:高

// :x:
await waitFor(() => {
  fireEvent.keyDown(input, {key: 'ArrowDown'})
  expect(screen.getAllByRole('listitem')).toHaveLength(3)
})

// :white_check_mark:
fireEvent.keyDown(input, {key: 'ArrowDown'})
await waitFor(() => {
  expect(screen.getAllByRole('listitem')).toHaveLength(3)
})

waitFor 适用的情况是:在执行的操作和断言之间存在不确定的时间量。因此,callback 可在不确定的时间和频率(在间隔以及 DOM 变化时调用)被调用(或者检查错误)。所以这也意味着你的副作用可能会被多次调用!

同时,这也意味着你不能在 waitFor 里面使用快照断言(SnapShot Assertion)。如果你想要用快照断言,首先要等待某些断言走完了,然后再拍快照。

建议:把副作用放在 waitFor 回调的外面,回调里只能有断言

用 get* 来做断言

重要程度:低

// :x:
screen.getByRole('alert', {name: /error/i})

// :white_check_mark:
expect(screen.getByRole('alert', {name: /error/i})).toBeInTheDocument()

虽然这不是什么大问题,但我还是想说下我的观点。如果 get* API 找不到元素,它就会抛出异常,打印整个 DOM 树结构(语法高亮),在 Debug 的时候很有用。也因为这点,断言是永远不可能失败的(因为如果找不到元素,查询在断言之前抛出异常)。

因为这个原因,很多人直接不做断言了。这其实也还好,但是我个人通常来说,会把断言留着,这样可以让后面做重构、修改的人知道:这里不是个查询操作,而是个断言操作。

建议:如果你想断言某个东西是否存在,那么就做显式的断言操作

总结

作为测试库工具系列的维护者,我们尽最大努力使 API 能够引导人们尽可能有效地使用,一些不足之处,我们会尝试正确地记录下来,即使这会非常地困难(尤其是 API 改动/升级等)。希望这篇文章会帮到你,我们只是想你更有信心地交付你的代码。

翻译:https://mp.weixin.qq.com/s/pgdcDNjDGPgNq76Zh_dZxg
原文:https://kentcdodds.com/blog/common-mistakes-with-react-testing-library

链接: https://fly63.com/article/detial/11319

解决Cannot read property range of null 错误

vue工程npm run serve/start/dev启动时,node_modules文件报:Cannot read property range of null 错误,该问题是babel-eslint版本更新问题导致的;

HTTP 400 错误 - 请求无效 (Bad request)

在ajax请求后台数据时有时会报 HTTP 400 错误 - 请求无效 (Bad request);出现这个请求无效报错说明请求没有进入到后台服务里;原因:前端提交数据的字段名称或者是字段类型和后台的实体类不一致

js异步错误捕获

我们都知道 try catch 无法捕获 setTimeout 异步任务中的错误,那其中的原因是什么。以及异步代码在 js 中是特别常见的,我们该怎么做才比较?

不能执行已释放Script的代码

父页面初始化声明变量a为数组(数组对象是引用类型,赋值传递的是地址),创建iframe子页面后给父页面变量a赋值,赋值后销毁iframe子页面,再次调用变量a的时候就会抛出异常‘SCRIPT5011:不能执行已释放Script的代码’。

JS错误处理:前端JS/Vue/React/Iframe/跨域/Node

js错误的实质,也是发出一个事件,处理他,error实例对象message:错误提示信息,name:错误名称(非标准属性)宿主环境赋予

nodejs提示 cross-device link not permitted, rename 错误解决方法

文件上传的功能时候,调用fs.renameSync方法错误,这个提示是跨区重命名文件出现的权限问题。先从源文件拷贝到另外分区的目标文件,然后再unlink,就可以了。

Js中使用innerHTML的缺点是什么?

如果在JavaScript中使用innerHTML,缺点是:内容随处可见;不能像“追加到innerHTML”一样使用;innerHTML不提供验证,因此我们可能会在文档中插入有效的和破坏性的HTML并将其中断

Web前端开发,必须规避的8个错误点!

现在,有越来越多所谓的“教程”来帮助我们提高网站的易用性。我们收集了一些在Web开发中容易出错和被忽略的小问题,并且提供了参考的解决方案,以便于帮助Web开发者更好的完善网站。

web前端错误监控

为什么要做前端错误监控?1. 为了保证产品的质量2. 有些问题只存在于线上特定的环境3. 后端错误有监控,前端错误没有监控,前端错误分为两类: 即时运行错误和资源加载错误

自定义错误及扩展错误

当我们在进行开发的时候,通常需要属于我们自己的错误类来反映任务中可能出现的特殊情况。对于网络操作错误,我们需要 HttpError,对于数据库操作错误,我们需要 DbError,对于搜索操作错误,我们需要 NotFoundError,等等

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!