大家好,我卡颂。
React18
进入大家视线曾经有一段时间了,不晓得各位有没有尝试并发个性呢?
当启用并发个性后,React
会从同步更新变为异步、带优先级、可中断的更新。
这也为编写单元测试带来了一些难度。
本文来聊聊React
团队如何测试并发个性。
欢送退出人类高质量前端框架群,带飞
遇到的窘境
次要有两个问题须要面对。
1. 如何表白渲染后果?
React
能够对接不同宿主环境的渲染器,大家最相熟的渲染器想必是ReactDOM
,用于对接浏览器与Node环境(SSR)。
对于一些场景,能够用ReactDOM
的输入后果做测试。
比方,上面是应用ReactDOM
的输入后果测试无状态组件的渲染后果是否合乎预期(测试框架是jest
):
it('should render stateless component', () => { const el = document.createElement('div'); ReactDOM.render(<FunctionComponent name="A" />, el); expect(el.textContent).toBe('A'); });
这里有个不不便的中央 —— 这个用例依赖浏览器环境
与DOM API
(比方用到document.createElement
)。
对于测试React外部运行机制这样的场景,掺杂了宿主环境相干信息显然会让测试用例编写起来更繁琐。
2. 如何测试并发环境?
如果将上文的用例中ReactDOM.render
改为ReactDOM.createRoot
,那么用例就会失败:
// 之前ReactDOM.render(<FunctionComponent name="A" />, el);expect(el.textContent).toBe('A');// 之后ReactDOM.createRoot(el).render(<FunctionComponent name="A" />);expect(el.textContent).toBe('A');
这是因为在新的架构下,很多同步更新变成了并发更新,当render
执行后,页面还没实现渲染。
要让上述用例胜利,最简略的批改形式是:
ReactDOM.createRoot(el).render(<FunctionComponent name="A" />);setTimeout(() => { // 异步获取后果 expect(el.textContent).toBe('A');})
如何优雅的应答这种变动?
React的应答策略
接下来咱们来看React
团队的应答形式。
首先来看第一个问题 —— 如何表白渲染后果?
既然ReactDOM
渲染器对应浏览器、Node
环境,ReactNative
渲染器对应Native
环境。
那能不能为测试外部运行流程专门开发一个渲染器呢?
答案是必定的。
这个渲染器叫React-Noop-Renderer
。
简略的说,这个渲染器会渲染出纯JS
对象。
实现一个渲染器
React
外部有个叫Reconciler
的包,他会援用一些操作宿主环境的API
。
比方如下办法用于向容器中插入节点:
function appendChildToContainer(child, container) { // 具体实现}
对于浏览器环境(ReactDOM
),应用appendChild
办法实现即可:
function appendChildToContainer(child, container) { // 应用appendChild办法 container.appendChild(child);}
打包工具(rollup
)将Reconciler
包与上述这类针对浏览器环境的API打包起来,就是ReactDOM
包。
在React-Noop-Renderer
中,与ReactDOM
中的DOM
节点对标的是如下数据结构:
const instance = { id: instanceCounter++, type: type, children: [], parent: -1, props};
留神其中的children
字段,用于保留子节点。
所以appendChildToContainer
办法在React-Noop-Renderer
中能够实现的很简略:
function appendChildToContainer(child, container) { const index = container.children.indexOf(child); if (index !== -1) { container.children.splice(index, 1); } container.children.push(child);};
打包工具将Reconciler
包与上述这类针对React-Noop的API打包起来,就是React-Noop-Renderer
包。
基于React-Noop-Renderer
,能够齐全脱离失常的宿主环境,测试Reconciler
外部的逻辑。
接下来来看第二个问题。
如何测试并发环境?
并发个性再简单,说到底也只是各种异步执行代码的策略,最终执行策略的API
不外乎setTimeout
、setInterval
、Promise
等。
在jest
中,能够模仿这些异步API
,管制他们的执行机会。
比方下面的异步代码,在React
中的测试用例会这么写:
// 测试用例批改后:await act(() => { ReactDOM.createRoot(el).render(<FunctionComponent name="A" />);})expect(el.textContent).toBe('A');
act
办法来自jest-react
包,他的外部会执行jest.runOnlyPendingTimers
办法,让所有期待中的计时器触发回调。
比方如下代码:
setTimeout(() => { console.log('执行')}, 9999999)
执行jest.runOnlyPendingTimers
后会立即打印执行。
通过这种形式,人为管制React
并发更新的速度,同时对框架代码0侵入。
除此之外,用于驱动并发更新的Scheduler
(调度器)模块,自身也有一个针对测试的版本。
在这个版本中,开发者能够手动管制Scheduler
的输出、输入。
比方,我想测试组件卸载时useEffect
回调的执行程序。
如上面代码所示,其中Parent
为挂载的被测试组件:
function Parent() { useEffect(() => { return () => Scheduler.unstable_yieldValue('Unmount parent'); }); return <Child />;}function Child() { useEffect(() => { return () => Scheduler.unstable_yieldValue('Unmount child'); }); return 'Child';}await act(async () => { root.render(<Parent />);});
依据yieldValue
的插入程序是否合乎预期,就能确定useEffect
的逻辑是否合乎预期:
expect(Scheduler).toHaveYielded(['Unmount parent', 'Unmount child']);
总结
React
中测试用例的编写策略为:
- 能够用
ReactDOM
测的用例,个别联合ReactDOM
与ReactTestUtils
(浏览器环境的辅助办法)实现 - 须要把控两头过程的用例,应用
Scheduler
的测试包,用Scheduler.unstable_yieldValue
记录过程信息 - 脱离宿主环境,独自测试
React
外部运行流程的,应用React-Noop-Renderer
- 测试并发下的场景,须要联合上述工具与
jest-react
一起应用
如果想深刻学习下React
中与测试相干的技巧,能够看下司徒正美老师的作品anu。
这是个类React
框架,但能跑通800+的React
用例。外面实现了ReactTestUtils
、React-Noop-Renderer
的简化版。
正美老师 R.I.P