当前位置: 首页 > 科技观察

React团队如何测试并发特性

时间:2023-03-11 20:29:17 科技观察

大家好,我是Kason。React18进入大家的视野已经有一段时间了。不知道你有没有试过“并发特性”?当启用“并发特性”时,React将从“同步更新”变为“异步、优先、可中断更新”。这也给编写单元测试造成了一些困难。本文讨论React团队如何测试并发特性。主要有两个问题需要面对。1、如何表达渲染结果?React可以连接到不同主机环境中的渲染器。最熟悉的渲染器一定是ReactDOM,它是用来连接“浏览器”和“节点环境”(SSR)的。对于某些场景,您可以使用ReactDOM的输出进行测试。例如下面是使用ReactDOM的输出来测试“stateless组件的渲染结果是否如预期”(测试框架是jest):it('shouldrenderstatelesscomponent',()=>{constel=document.createElement('div');ReactDOM.render(,el);expect(el.textContent).toBe('A');});难点在于——此用例依赖于浏览器环境和DOMAPI(例如使用document.createElement)。对于测试“React内部运行机制”这样的场景,添加宿主环境的相关信息显然会让编写测试用例变得更加繁琐。2、如何测试并发环境?如果在上述用例中将ReactDOM.render更改为ReactDOM.createRoot,则用例将失败://BeforeReactDOM.render(,el);expect(el.textContent).toBe('A');//在ReactDOM.createRoot(el).render()之后;期望(el.textContent).toBe('A');这是因为在新架构下,很多“同步更新”变成了“并发更新”。执行渲染时,页面尚未渲染。要使上述用例成功,最简单的修改方法是:ReactDOM.createRoot(el).render();setTimeout(()=>{//异步获取结果expect(el.textContent).toBe('A');})如何优雅地处理这个变化?React的应对策略接下来我们看看React团队是如何应对的。我们先来看第一个问题——如何表达渲染结果?由于ReactDOM渲染器对应的是浏览器和Node环境,所以ReactNative渲染器对应的是Native环境。是否可以专门开发一个渲染器来测试“内部运行过程”?答案是肯定的。这个渲染器叫做React-Noop-Renderer。简单地说,这个渲染器将渲染纯JS对象。实现渲染器React内部有一个名为Reconciler的包,它会引用一些“操作宿主环境”的API。例如,下面的方法用于“将节点插入到容器中”:functionappendChildToContainer(child,container){//具体实现}对于浏览器环境(ReactDOM),使用appendChild方法:functionappendChildToContainer(child,container){//使用appendChild方法container.appendChild(child);}打包工具(rollup)将Reconciler包与上述“浏览器环境的API”打包在一起,即ReactDOM包。在React-Noop-Renderer中,如下数据结构与ReactDOM中的DOM节点对齐:constinstance={id:instanceCounter++,type:type,children:[],parent:-1,props};注意children字段,用于存放子节点。所以appendChildToContainer方法可以非常简单地在React-Noop-Renderer中实现:if(index!==-1){container.children.拼接(索引,1);}container.children.push(孩子);};打包工具将Reconciler包与上述“APIforReact-Noop”打包在一起,即React-Noop-Renderer包。基于React-Noop-Renderer,可以完全脱离正常的宿主环境,测试Reconciler内部的逻辑。接下来我们来看第二个问题。如何测试并发环境?“并发特性”再复杂,归根结底也不过是“异步执行代码的各种策略”。最终执行策略的API无非就是setTimeout、setInterval、Promise等,开玩笑的是,可以模拟这些异步API来控制它们的执行时机。比如上面的异步代码,React中的测试用例会这样写://测试用例修改后:awaitact(()=>{ReactDOM.createRoot(el).render();})期望(el.textContent).toBe('A');act方法来自于jest-react包,它会在内部执行jest.runOnlyPendingTimers方法让所有等待的定时器触发回调。例如下面的代码:setTimeout(()=>{console.log('execution')},9999999)会在执行jest.runOnlyPendingTimers后立即打印“execution”。这样就人为控制了React并发更新的速度,同时零侵入了框架代码。另外,用于驱动并发更新的调度器(scheduler)模块也有测试用的版本。在这个版本中,开发者可以手动控制Scheduler的输入输出。例如,我想测试组件卸载时useEffect回调的执行顺序。如下代码所示,其中Parent为挂载的“被测组件”:return;}functionChild(){useEffect(()=>{return()=>Scheduler.unstable_yieldValue('Unmountchild');});return'Child';}awaitact(async()=>{root.render();});根据yieldValue的插入顺序是否符合预期,可以判断useEffect的逻辑是否符合预期:expect(Scheduler).toHaveYielded(['Unmountparent','Unmountchild']);总结React中测试用例的编写策略:可以用ReactDOM测试的用例,一般结合ReactDOM和ReactTestUtils(浏览器环境的辅助方法)来完成需要控制中间过程的用例,使用测试包调度程序,并使用调度程序。unstable_yieldValue记录宿主环境外的进程信息,单独测试React内部运行进程,使用React-Noop-Renderer测试并发场景。需要和上面的工具一起使用,jest-react。如果想深入了解React中测试相关的技巧,可以看看司徒正妹老师的作品anu[1]。这是一个类似React的框架,但它可以运行800+个React用例。其中实现了ReactTestUtils和React-Noop-Renderer的简化版本。