[Day29] React Testing Library 的一些实用的小技巧

今天来谈谈 React Testing Library 中笔者常用到的一些功能,React Testing Library 的套件名是 @testing-library/react,它是奠基在 Testing Library 提供的许多方法上,为 React 的测试提供了更多不同的方法。如果读者写的不是 React,Testing Library 本身也搭配很多不同的框架可以用来撰写测试,例如,Angular、Vue、Svelte、Puppeteer、Cypress 等等。

同样的,笔者不会说明如何使用 react-testing-library 来撰写 React 元件的测试,如果有需要的话,推荐可以看 Youtube 上这系列的教学影片 React Testing Library Tutorial

getBy、queryBy、findBy 的使用时机

在 React Testing Library 中提供了三种用来 query DOM 元素的方法,分别是 getByqueryBy、和 findBy,在官方文件中用详细的表格来说明这三种方法的差异:

About Queries

图表资料来自官方网站:About Queries @ testing-library/react

但这张表真正想告诉我们的是什麽呢?这里笔者整理各个方法的使用时机重点如下:

  • 使用 queryBy找不到该元素时不会喷错,通常是要用来检查某个元素「不在 DOM 上」时使用
  • 使用 findBy需要搭配 async/await 时,通常该 DOM 元素不是一开始就 mount 在页面上
  • 使用 getBy:除了上述情况之外,都可以用 getBy,getBy 在找不到该元素时会直接喷错(throw Error)

除了这三个方法的差异外,再来就是开发者要找的是「单一个元素(xxxBy)」或「多个元素(xxxAllBy)」:

  • 如果使用的不是 xxxAllBy 的方法,但却找到超过一个以上的元素时会喷错
  • 使用 xxxAllBy 方法时会回传的是阵列

如何快速找到特定元素(query role attribute)

由於 Testing Library 很强调用实际使用者的视角来进行测试,因此它会更偏好开发者使用 DOM 上符合 Accessibility 的元素(例如,role、label)、或者是使用者实际上看得的东西(例如、Text、Title)来找到欲进行测试的元素,而不是透过使用者看不到的 class 或 id 来进行 query。

注:不是不能使用 id 来 query 元素,有些时候只能用 id 或直接用 id 会更有效率,只是以偏好来说,Testing Library 更建议使用符合 Accessibility 方法。

但实务上来说,一般的开发者可能并不清楚每个 HTML 元素所对应的 ARIA role 是什麽,这时候有几个不同的方法可以处理。

使用 Chrome 的开发者工具

第一种方式是使用 Chrome 内建的开发者工具,在 Chrome 的开发者工具,按下「Alt + Shift + P」後,搜寻 show accessibility,接着在 Accessibility 页签下的 Computed Properties 中就会显示改元素的 role 和 name:

react testing library

如此就可以使用 React Testing Library 提供的 getByRole 方法来选到该元素:

// App.test.tsx
import { render, screen } from './custom-testing-library';
import App from './App';

test('can find the specific text in specific DOM', async () => {
  render(<App />);

  // 使用 getByRole 方法
  const heading = screen.getByRole('heading', {
    name: /your current path is \//i,
  });

  expect(heading).toBeInTheDocument();
});

使用 logRole 方法

或者也可以使用 Testing Library 内建的 logRole API。我们只需要把想检视的 HTML Element 放入 logRole 这个方法中,Testing Library 就会告诉开发者在这个 HTML 元素中有哪些 ARIA 的 role 可以使用。

举例来说,在测试的档案中:

// App.test.tsx
import { logRoles, render, screen } from '@testing-library/react';

test('can find the specific text in specific DOM', async () => {
  const { container } = render(<App />);

  // 使用 logRoles 来检视某个 HTML Element 所包含的 Accessibility Role
  logRoles(container);
  // ...
});

在 Terminal 中就可以看到所有这个 DOM 中的 ARIA role 和对应的 name,例如这里包含了两个 role,分别是 bannerheading

react testing library

使用 Chrome Extension

最後一种,应该也是最简单的方式是直接透过 Chrome Extension,在 5 Tips to Perfect React Testing Library Queries 这篇文章中推荐了两个好用的 Chrome Extension,分别如下:

Testing Library: which query

Testing Library: which query @ Chrome Extension

这个套件可以直接把要 query 的元素装成 Testing Library 的写法後,用点右键的方式复制下来:

react testing library

按下复制後,就可以取得下列程序:

screen.getByRole('heading', { name: /your current path is \//i });

Testing Playground

Testing Playground @ Chrome Extension

另一个套件是 Testing Playground,它会在 Chrome 的开发者工具中多一个 Tab,当你选了特定元素後,一样会出现可以复制的程序码,除此之外,最下面还会列出和 Accessible 有关的属性:

Testing Playground

这两套都可以方便开发者找到想要的元素。

透过 Debug 把 DOM console 出来

screen.debug()

screen.debug() 这个满实用也蛮多人知道的,基本上就是可以把当前画面的 DOM 显示在 Terminal 中:

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

test('can find the specific text in specific DOM', async () => {
  render(<App />);

  screen.debug();
});

此时的 Terminal 会得到如下的结果:

screen debug

prettyDOM

虽然用 screen.debug() 可以看到目前 DOM 的样子,但因为它有行数限制,当 Component 转译出来的 DOM 很多行时,就没有办法看到完整的内容。这时候就可以使用 Testing Library 提供的 prettyDOM 这个方法来把特定的 DOM 元素 console 出来。

为什麽不直接使用 console.log() 就好呢?因为会非常难看。举例来说,现在我们找到了 heading 这个元素,想要把它 console 出来看一下:

// App.test.tsx
import { render, screen } from '@testing-library/react';
test('can find the specific text in specific DOM', async () => {
  render(<App />);

  const heading = screen.getByRole('heading', {
    name: /your current path is \//i,
  });

  // 使用原本的 console.log
  console.log(heading);
});

这时候 console 出来的内容会像这样:

prettyDOM

非常难以阅读实际的 DOM 会长什麽样,但如果是先用 Testing Library 提供的 prettyDOM 方法後再执行 console,像是这样:

// App.test.tsx

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

test('can find the specific text in specific DOM', async () => {
  render(<App />);

  const heading = screen.getByRole('heading', {
    name: /your current path is \//i,
  });

  // 先使用 prettyDOM 後再 console
  console.log(prettyDOM(heading));
});

这时候 console 出来的结果如下:

prettyDOM

是不是容易阅读的多了。

在 Production 移除 test-id

最後,除了使用 Accessible Attribute 或画面上的文字来 query DOM 元素之外,有时还是必须使用 id 的方式来 query DOM,Testing Library 预设可以使用 getByTestId 这个方法来找出在 DOM 元素带有特定 data-testid 的 HTML 元素。

举例来说,可以在想要 query 到的 DOM 元素加上 data-testid="heading"

<h1 data-testid="heading">Your current path is {location.pathname}</h1>

这时候在写测试时,就可以使用 getByTestId 这个方法:

import { render, screen } from './custom-testing-library';
import App from './App';

test('can find the specific text in specific DOM', async () => {
  render(<App />);

  const headingElement = screen.getByTestId('heading');
  expect(headingElement).toBeInTheDocument();
});

这麽做虽然很方便,但在 Production 的产品上,总是不希望留下这些 data-testid,一来不太好看,二来实在是给爬虫一个很大的方便。因此,如果希望能在 production 时移除 data-testid 这个属性,在官方文件中提到可以透过 babel 的 babel-plugin-react-remove-properties 来解决,这个 plugin 可以在 bundle 成 production 时,把所有指定的 attribute 的移掉,因此我们也可以利用这个 plugin 来把为了测试而写的 data-testid 移除。

使用时需要在 babel 的设定档(.babelrc)中加上下列设定:

// .bablerc
{
  "env": {
    "production": {
      "plugins": ["react-remove-properties"]
    }
  }
}

预设就会把名称是以 data-test 开头的属性都移除,但如果读者不是用预设的 data-testid 作为 query 的属性,或者你想透过这个 babel plugin 移除掉其他的 DOM attribute 也是可以的,方式也很简单,可以直接参考该套件的使用说明

小结

在这次的铁人赛中,笔者仅用大约一周的时间分享撰写测试时的一些经验,实际上测试能撰写的内容,不论是概念或实务都远超过此,在此次铁人赛中也有多位参赛者是撰写和测试有关的题目,如果读者对於测试想要有更多了解,也欢迎去阅读这些内容。

最後,还是鼓励大家从「为你自己开始写测试」,程序是你写的,而你必须为你写的程序负责。

参考资料


<<:  第29车厢-倒数一篇!人人有奖~抽奖抽起来了各位!

>>:  Day29: Picker controller

【D4】汇入交易资料

前言 这是需要调整资料库连线资讯,修改成符合MySQL的格式。 本次也会参考《【Day10】Azur...

Day 14 JavaScript interop

虽然 Blazor 不需要用到 JavaScript,但某些已有的 library 还是很方便,不能...

03 - Uptime - 掌握系统的生命徵象 (1/4) - 我们要观测的生命徵象是什麽?

Uptime - 掌握系统的生命徵象 系列文章 (1/4) - 我们要观测的生命徵象是什麽? (2/...

【Day15】Enzyme的两个常用渲染API及Jest的几个API,和..设计测试的几个要点 (・θ・)

为了让我们的测试看起来乾净,就跟写Code一样, 浅显易懂是原则,所以我们要把握以下两点! DRY(...

Day 25:动态规划(dynamic programming)

动态规划也是一种演算法设计模式,常用来解决最佳化问题。它的方法是将问题(通常是递回地)分解成子问题,...