Browse Source

site: add blog about test library migration (#39591)

* add blog

* fix

* fix

* fix lint

* fix

* fix

* fix

* fix

* fix

* fix
pull/39605/head
lijianan 2 years ago
committed by GitHub
parent
commit
0eaf2b40cf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      .dumi/theme/slots/Content/index.tsx
  2. 2
      docs/blog/check-conduct.en-US.md
  3. 291
      docs/blog/testing-migrate.en-US.md
  4. 291
      docs/blog/testing-migrate.zh-CN.md

11
.dumi/theme/slots/Content/index.tsx

@ -195,11 +195,12 @@ const Content: React.FC<{ children: ReactNode }> = ({ children }) => {
<CalendarOutlined /> {DayJS(meta.frontmatter.date).format('YYYY-MM-DD')}
</span>
)}
{meta.frontmatter.author && (
<Typography.Link href={`https://github.com/${meta.frontmatter.author}`}>
@{meta.frontmatter.author}
</Typography.Link>
)}
{meta.frontmatter.author &&
(meta.frontmatter.author as string)?.split(',')?.map((author) => (
<Typography.Link href={`https://github.com/${author}`} key={author}>
@{author}
</Typography.Link>
))}
</Space>
</Typography.Paragraph>
) : null}

2
docs/blog/check-conduct.en-US.md

@ -1,5 +1,5 @@
---
title: Tree 的勾选传导
title: Tree's check conduction
date: 2022-12-14
author: zombieJ
---

291
docs/blog/testing-migrate.en-US.md

@ -0,0 +1,291 @@
---
title: about antd test library migration
date: 2022-12-16
author: zombieJ,li-jia-nan
---
Hello, I am [@li-jia-nan](https://github.com/li-jia-nan). I also joined the antd Collaborator a few months ago. Fortunately, as one of the Collaborators, I developed the **FloatButton component** and **QRCode component**, as well as some other maintenance work. Let me share the migration of the antd test library son~
## introduction
In `antd@4.x`, **[enzyme](https://enzymejs.github.io/enzyme)** is used as the test framework. However, due to the lack of maintenance of enzyme, it is difficult to support it in the React 18 era . Therefore, I had to start a long **[@testing-lib](https://testing-library.com/docs/react-testing-library/intro)** migration road for antd.
During the migration process, I undertook about a quarter of the workload of antd. Here I mainly record the problems encountered during the migration process.
> Thanks for the time [@zombieJ](https://github.com/zombieJ) [@MadCcc](https://github.com/MadCcc) [@miracles1919](https://github.com/miracles1919) for help.
![image](https://user-images.githubusercontent.com/49217418/207530591-1faaf171-638b-40af-8d61-3f07cb60abe2.png)
![image](https://user-images.githubusercontent.com/49217418/207530491-4988ecc4-2da0-4a0c-ba5d-b9797edecd1a.png)
![image](https://user-images.githubusercontent.com/49217418/207530507-412f0244-3d88-4500-9eb4-054f3e112731.png)
## start
Before migrating, we need to figure out what the purpose of the migration is. In `enzyme`, most scenarios are to test whether the state in the component is correct, or whether the static properties on the class are assigned normally, which is actually unreasonable, because we need to care more about whether the "function" is normal , rather than whether the "attribute" is correct, because the source code is a black box for the user, and the user only cares about whether the component is correct.
Basically, test cases should be written based on "behavior", not "implementation" (this is also the goal of `testing-library`). In principle, several use cases were found to be redundant (because some functions would not be triggered individually in real code), and their removal did not affect the test coverage.
Of course, this is only one of the reasons to drop `enzyme`. More importantly it is unmaintained and does not support React 18 anymore.
## migrate
### 1. render
`enzyme` supports rendering in three ways:
- shallow: Shallow rendering, which is an encapsulation of the official Shallow Renderer. Render the component into a virtual DOM object. The component obtained through Shallow Render will not have a part asserted to the sub-component, and the information of the component can be accessed using jQuery.
- render: Static rendering, which renders the React component into a static HTML string, then parses the string, and returns an instance object, which can be used to analyze the html structure of the component.
- mount: Fully rendered, it loads component rendering into a real DOM node to test the interaction of DOM API and the life cycle of components, and uses jsdom to simulate the browser environment.
In order to be close to the real scene of the browser, `antd@4.x` uses `mount` for rendering, and the corresponding `render` method in `@testing-library`:
```diff
-- import { mount } from 'enzyme';
++ import { render } from '@testing-library/react';
-- const wrapper = mount(
++ const { container } = render(
<ConfigProvider getPopupContainer={getPopupContainer}>
<Slider />
</ConfigProvider>,
);
```
### 2. interact & event
`enzyme` provides `simulate(event)` method to simulate event triggering and user interaction, `event` is the name of the event, and the corresponding `fireEvent` method in `@testing-library`:
```diff
++ import { fireEvent } from '@testing-library/react';
-- wrapper.find('.ant-handle').simulate('click');
++ fireEvent.click(container.querySelector('.ant-handle'));
```
### 3. DOM element
In `enzyme`, some built-in APIs are provided to manipulate dom, or find components:
- instance(): Returns an instance of the test component
- at(index): returns a rendered object
- text(): Returns the text content of the current component
- html(): Returns the HTML code form of the current component
- props(): Returns all properties of the component
- prop(key): Returns the specified property of the component
- state(): Returns the state of the component
- setState(nextState): Set the state of the component
- setProps(nextProps): Set the properties of the component
- find(selector): Find the node according to the selector, the selector can be the selector in CSS, or the constructor of the component, and the displayName of the component, etc.
`testing-library` 中,没有提供这些 api(正如上面提到过的 - `testing-library` 更加注重行为上的测试),所以需要换成原生的 dom 操作:
```diff
expect(ref.current.getPopupDomNode()).toBe(null);
-- popover.find('span').simulate('click');
-- expect(popover.find('Trigger PopupInner').props().visible).toBeTruthy();
++ expect(container.querySelector('.ant-popover-inner-content')).toBeFalsy();
++ fireEvent.click(popover.container.querySelector('span'));
++ expect(container.querySelector('.ant-popover-inner-content')).toBeTruthy();
```
### 4. compatibility test
While the major version is being upgraded, some components are discarded, but they are not removed in antd. For example, the BackTop component needs to add warning to the component to ensure compatibility, so it is also necessary to write a special unit test for warning:
```diff
describe('BackTop', () => {
++ it('should console Error', () => {
++ const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
++ render(<BackTop />);
++ expect(errSpy).toHaveBeenCalledWith(
++ 'Warning: [antd: BackTop] `BackTop` is deprecated, please use `FloatButton.BackTop` instead.',
++ );
++ errSpy.mockRestore();
++ });
});
```
During the conversion process, I discovered a magical phenomenon. In some cases, the DOM snapshot generated by the same case will be different, so I began to explore what has changed in React 18:
## Diff Mystery
In the past, the `snapshot` comparison of `enzyme` was to convert the `enzyme object` into a serialized object through the `enzyme-to-json` plugin:
```js
// jest.config.js
module.exports = {
// ...
snapshotSerializers: ['enzyme-to-json/serializer'],
};
```
When it comes to `@testing-library/react`, directly call `render` to generate dom elements, and then compare the dom:
```diff
-- import { mount } from 'enzyme';
++ import { render } from '@testing-library/react';
describe('xxx', () => {
it('yyy', () => {
-- const wrapper = mount(<Demo />);
++ const { container } = render(<Demo />);
-- expect(wrapper.render()).toMatchSnapshot();
++ expect(container.firstChild).toMatchSnapshot();
});
});
```
Interestingly, in some test cases. It will hang, the difference is that React 18 sometimes has fewer blank lines:
```diff
<div>
--
Hello World
</div>
```
After testing `innerHTML` of dom, it is found that 17 and 18 are the same. So at the beginning of the problem, we simply changed the test case to compare `innerHTML`:
```ts
expect(container.querySelector('.className').innerHTML).toMatchSnapshot();
```
However, as you migrate more, you will gradually see this happening over and over again. Comparing `innerHTML` is also not a long-term solution. So began to explore why this happens.
## pretty-format
`pretty-format` is an interesting library that converts any object into a string. One of its uses is for snapshot comparison of jest. One of its features is that conversion rules can be customized.
Compared with `snapshot` in `jest`, `format` will be done first, for common objects such as native `dom`, `object`. It has built-in a set of `plugins` for format conversion:
```html
<div>
<span>Hello</span>
<p>World</p>
</div>
<div>
<span> Hello </span>
<p>World</p>
</div>
```
The first reaction to the appearance of extra spaces is whether it is because the version of `@testing-lib/react` introduced by 17 & 18 is different, which affects the version of `pretty-format` that `jest` depends on. After checking, they are all consistent:
```json
{
"devDependencies": {
"pretty-format": "^29.0.0",
"@testing-library/react": "^13.0.0"
}
}
```
After this judgment is wrong, it is another situation. There is an `empty element` in the dom, which makes `pretty-format` perceptible, but it does not affect `innerHTML`, so I wrote a simple test case:
```ts
const holder = document.createElement('div');
holder.append('');
holder.append(document.createElement('a'));
expect(holder).toMatchSnapshot();
console.log(holder.innerHTML);
```
and get the following output:
```snap
// snapshot
exports[`debug exports modules correctly 1`] = `
<div>
<a />
</div>
`;
// console.log
<a></a>
```
Consistent with the idea, then it is very simple. Then there is a high probability that the `render` of `React 18` will ignore empty elements. Let's do a simple experiment:
```tsx
import React, { version, useRef, useEffect } from 'react';
const App: React.FC = () => {
const holderRef = useRef<HTMLDivElement>(null);
useEffect(() => {
console.log(holderRef.current?.childNodes);
}, []);
return (
<div ref={holderRef}>
<p>{version}</p>
</div>
);
};
export default App;
```
as predicted:
| React 17 | React 18 |
| ----------------------- | -------------- |
| NodeList(2) \[text, p\] | NodeList \[p\] |
Check the `Fiber` node information, you can find that `React 17` will treat empty elements as `Fiber` nodes, while `React 18` will ignore empty elements:
> React 17:
![image](https://user-images.githubusercontent.com/49217418/207533725-fb8f9e4d-7f09-4a13-a04a-cbb2d3eb2aea.png)
> React 18:
![image](https://user-images.githubusercontent.com/49217418/207533740-328d10ea-d9bc-4469-bc00-f08e33857e6f.png)
You can find the relevant PR by following the map:
- https://github.com/facebook/react/pull/22807
![QQ20221206-204507](https://user-images.githubusercontent.com/49217418/205916780-1ef901df-f15d-453d-a3ce-54b87e045dad.png)
## a solution
Antd needs to test React16, 17, and 18. If snapshot is not feasible, it will cause too much cost. So we need to modify jest. `enzyme-to-json` gave me inspiration, we can modify the snapshot generation logic to smooth out the diff between different versions of React:
```ts
expect.addSnapshotSerializer({
// Determine whether it is a dom element, if yes, go to our own serialization logic
// The code has been simplified, more logic is needed for real judgment, you can refer to setupAfterEnv.ts of antd
test: (element) => element instanceof HTMLElement,
// ...
});
```
Then access `pretty-format` and add your own logic:
```ts
const htmlContent = format(element, {
plugins: [plugins.DOMCollection, plugins.DOMElement],
});
expect.addSnapshotSerializer({
test: '//...',
print: (element) => {
const filtered = htmlContent
.split(/[\n\r]+/)
.filter((line) => line.trim())
.map((line) => line.replace(/\s+$/, ''))
.join('\n');
return filtered;
},
});
```
## knock off
The above are some problems encountered during the migration of the antd test framework. I hope to help students who need to migrate or have not yet started writing test cases. Everyone is also welcome to join the antd community and contribute to open source together.

291
docs/blog/testing-migrate.zh-CN.md

@ -0,0 +1,291 @@
---
title: antd 测试库迁移的那些事儿
date: 2022-12-16
author: zombieJ,li-jia-nan
---
大家好,我是 [@li-jia-nan](https://github.com/li-jia-nan)。也是前几个月新加入的 antd Collaborator, 有幸作为 Collaborators 之一,我开发了 **FloatButton 组件****QRCode 组件**,以及一些其它维护工作,下面分享一下 antd 测试库迁移的那些事儿~
## 引言
`antd@4.x` 中,使用 **[enzyme](https://enzymejs.github.io/enzyme)** 作为测试框架,然而由于 enzyme 缺乏维护,到了 React 18 时代已经很难⽀持。也因此不得不开始为 antd 开启漫⻓的 **[@testing-lib](https://testing-library.com/docs/react-testing-library/intro)** 迁移之路。
在迁移过程中,我承担了大概 antd 四分之一的工作量,这里主要记录一下迁移过程中遇到的问题。
> 感谢在此期间 [@zombieJ](https://github.com/zombieJ) [@MadCcc](https://github.com/MadCcc) [@miracles1919](https://github.com/miracles1919) 提供的帮助。
![image](https://user-images.githubusercontent.com/49217418/207530591-1faaf171-638b-40af-8d61-3f07cb60abe2.png)
![image](https://user-images.githubusercontent.com/49217418/207530491-4988ecc4-2da0-4a0c-ba5d-b9797edecd1a.png)
![image](https://user-images.githubusercontent.com/49217418/207530507-412f0244-3d88-4500-9eb4-054f3e112731.png)
## 起步
在迁移之前,我们需要先搞清楚迁移的目的是什么。在 `enzyme` 中,大多数场景是测试了组件中的状态是否正确,或者 class 上的静态属性是否正常被赋值,这其实是不合理的,因为我们更重要的是需要关心“功能”是否正常,而非“属性”是否正确,因为源代码对使用者来说是黑盒,用户只关心组件是否正确。
基上,测试用例应该基于“行为”来编写,而非“实现”来编写(这也是 `testing-library` 的目标)。在这个原则上,会发现有几个用例是多余的(因为在实际代码中不会单独触发某些函数),将其删除也并没有影响到 test coverage。
当然了,这只是放弃 `enzyme` 的其中一个原因。更重要的是它缺乏维护,并且不支持 React 18 了。
## 迁移
### 一、渲染:
`enzyme` 支持三种方式的渲染:
- shallow: 浅渲染,是对官方的 Shallow Renderer 的封装。将组件渲染成虚拟 DOM 对象,通过 Shallow Render 得到的组件不会有断言到子组件的部分,并且可以使用 jQuery 的方式访问组件的信息。
- render: 静态渲染,它将 React 组件渲染成静态的 HTML 字符串,然后解析这段字符串,并返回一个实例对象,可以用来分析组件的 html 结构。
- mount: 完全渲染,它将组件渲染加载成一个真实的 DOM 节点,用来测试 DOM API 的交互和组件的生命周期,用到了 jsdom 来模拟浏览器环境。
为了贴近浏览器现实场景,`antd@4.x` 选用 `mount` 来进行渲染,而在 `@testing-library` 中对应的则是 `render` 方法:
```diff
-- import { mount } from 'enzyme';
++ import { render } from '@testing-library/react';
-- const wrapper = mount(
++ const { container } = render(
<ConfigProvider getPopupContainer={getPopupContainer}>
<Slider />
</ConfigProvider>,
);
```
### 二、交互 & 事件
`enzyme` 提供了 `simulate(event)` 方法来模拟事件触发和用户交互,`event` 为事件名称,而在 `@testing-library` 中对应的则是 `fireEvent` 方法:
```diff
++ import { fireEvent } from '@testing-library/react';
-- wrapper.find('.ant-handle').simulate('click');
++ fireEvent.click(container.querySelector('.ant-handle'));
```
### 三、DOM 元素
`enzyme` 中,提供了一些内置的 api 来操作 dom,或者查找组件:
- instance(): 返回测试组件的实例
- at(index): 返回一个渲染过的对象
- text(): 返回当前组件的文本内容
- html(): 返回当前组件的 HTML 代码形式
- props(): 返回组件的所有属性
- prop(key): 返回组件的指定属性
- state(): 返回组件的状态
- setState(nextState): 设置组件的状态
- setProps(nextProps): 设置组件的属性
- find(selector): 根据选择器查找节点,selector 可以是 CSS 中的选择器,也可以是组件的构造函数,以及组件的 displayName 等
`testing-library` 中,没有提供这些 api(正如上面提到过的 - `testing-library` 更加注重行为上的测试),所以需要换成原生的 dom 操作:
```diff
expect(ref.current.getPopupDomNode()).toBe(null);
-- popover.find('span').simulate('click');
-- expect(popover.find('Trigger PopupInner').props().visible).toBeTruthy();
++ expect(container.querySelector('.ant-popover-inner-content')).toBeFalsy();
++ fireEvent.click(popover.container.querySelector('span'));
++ expect(container.querySelector('.ant-popover-inner-content')).toBeTruthy();
```
### 四、兼容性测试
在大版本升级的同时,废弃了部分组件,但是并没有在 antd 中移除,比如 BackTop 组件,需要在组件中加入 warning 以保证兼容性,所以还需要对 warning 编写专门的单元测试:
```diff
describe('BackTop', () => {
++ it('should console Error', () => {
++ const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
++ render(<BackTop />);