共计 3308 个字符,预计需要花费 9 分钟才能阅读完成。
大家好,我卡颂。
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