 的完整指南:在浅渲染下测试 Render Prop 组件)
测试前端【免费下载链接】enzymeJavaScript Testing utilities for React项目地址https://gitcode.com/gh_mirrors/en/enzyme点击查看免费下载导读在 React 测试中Render Prop渲染属性模式通过函数类型的 prop 让父组件把渲染能力下放给子组件Mouse render{(x, y) ...} /便是典型代表。Enzyme 的ShallowWrapper.renderProp(propName)(...args)正是为此而生的测试 API它能把任意函数类型的 prop 当作渲染函数来调用并将该函数返回的 React 元素包装成新的ShallowWrapper从而让你在不触发真实挂载mount的前提下直接断言 Render Prop 的输出结构。读完本文你将掌握renderProp()的完整签名、调用链、约束条件与错误处理并能用它与find()、equals()、invoke()、dive()等 API 组合出可维护的浅渲染测试用例。本文以官方 API 文档 renderProp.md 为主体并结合 ShallowWrapper.js 中的源码实现与 renderProp.jsx 中的测试用例进行印证。一、API 签名与返回值的本质renderProp()的完整 API 签名如下.renderProp(propName)(...args) ShallowWrapper调用renderProp(propName)会返回一个函数随后用args调用这个返回的函数会得到一个新的ShallowWrapper它包装的是“原始 wrapper 中名为propName的那个 proprender prop被以args作为参数调用后所返回的 React 节点”。从参数与返回值角度看项目说明propNameString要调用的 render prop 的属性名必须是字符串...argsArrayAny传递给 render prop 函数的实参列表可以为空、单个或多个返回值ShallowWrapper包装了 render prop 返回的节点官方文档给出了一句非常精辟的等价表述“This essentially callswrapper.prop(propName)(...args).”也就是说renderProp本质上等价于先通过prop()取出该函数 prop再以args直接调用它。两者的差异在于renderProp额外做了包装与类型校验详见第三节源码解析并把调用结果封装成可继续链式操作的 wrapper而不是返回裸的 React 元素。需要特别强调的是使用前提renderProp()只能被调用在包装了单个、非 DOMcustom component节点的 wrapper上。也就是说wrapper 必须恰好包含一个节点length 1该节点必须是自定义组件class 或 function 组件不能是宿主 DOM 元素如div。二、完整示例从零测试一个 Mouse Render Prop 组件官方文档提供了一个非常经典、可直接运行的完整用例下面完整保留并逐段展开。测试夹具被测试的 Render Prop 组件首先是负责“捕获鼠标位置”的Mouse组件——它自己不渲染内容而是把状态{ x, y }通过名为render的 prop 交给调用方class Mouse extends React.Component { constructor() { super(); this.state { x: 0, y: 0 }; } render() { const { render } this.props; return ( div style{{ height: 100% }} onMouseMove{(event) { this.setState({ x: event.clientX, y: event.clientY, }); }} {render(this.state)} /div ); } } Mouse.propTypes { render: PropTypes.func.isRequired, };然后是消费方App它把render这个函数 prop 传给Mouse。这里(x 0, y 0) ...为参数提供了默认值因此“无参调用”也能得到确定的输出function App() { return ( div style{{ height: 100% }} Mouse render{(x 0, y 0) ( h1 The mouse position is ({x}, {y}) /h1 )} / /div ); }用例一无参调用 render propconst wrapper shallow(App /) .find(Mouse) .renderProp(render)(); expect(wrapper.equals(h1The mouse position is 0, 0/h1)).to.equal(true);这段测试的调用链值得拆解shallow(App /)得到根 wrapper.find(Mouse)定位到被渲染出来的Mouse节点此时 wrapper 内只有一个自定义组件节点满足调用前提.renderProp(render)()取出render这个函数 prop以零参数调用它得到h1The mouse position is (0, 0)/h1元素并包装成新的 wrapper.equals(...)断言新 wrapper 与期望元素结构完全相等。由于App传入的 render 函数带有默认值x 0, y 0所以即使Mouse的state尚未被鼠标事件更新无参调用也能渲染出确定内容。用例二带多个参数调用 render propconst wrapper shallow(App /) .find(Mouse) .renderProp(render)(10, 20); expect(wrapper.equals(h1The mouse position is 10, 20/h1)).to.equal(true);这里向 render prop 传入了10和20两个实参等价于直接执行props.render(10, 20)返回的h1随之更新为(10, 20)。这一能力让测试可以绕过Mouse的内部 state直接用任意坐标值驱动渲染输出从而把“状态如何产生”与“渲染如何消费状态”解耦开来分别验证。三、源码实现解析renderProp到底做了什么renderProp的实现位于 packages/enzyme/src/ShallowWrapper.js#L1340-L1368完整代码如下renderProp(propName) { const adapter getAdapter(this[OPTIONS]); if (typeof adapter.wrap ! function) { throw new RangeError(your adapter does not support wrap. Try upgrading it!); } return this.single(renderProp, (n) { if (n.nodeType host) { throw new TypeError(ShallowWrapper::renderProp() can only be called on custom components); } if (typeof propName ! string) { throw new TypeError(ShallowWrapper::renderProp(): propName must be a string); } const props this.props(); if (!hasOwn(props, propName)) { throw new Error(ShallowWrapper::renderProp(): no prop called “${propName}“ found); } const propValue props[propName]; if (typeof propValue ! function) { throw new TypeError(ShallowWrapper::renderProp(): expected prop “${propName}“ to contain a function, but it holds “${typeof propValue}“); } return (...args) { const element propValue(...args); const wrapped adapter.wrap(element); return this.wrap(wrapped, null, this[OPTIONS]); }; }); }从源码结构看其内部工作流程可以概括为以下六步适配器能力检查首先通过getAdapter(this[OPTIONS])拿到当前适配器并要求其实现了wrap方法否则抛出RangeError提示 “your adapter does not supportwrap”。这意味着较旧的 adapter 可能不支持该 API。单节点约束整个逻辑被包在this.single(renderProp, fn)中。single会先校验当前 wrapper 只包装了一个节点不满足时直接抛错从而保证了“只能对单节点 wrapper 调用”。非宿主节点校验通过n.nodeType host判断当前节点是否为 DOM 宿主元素若是则抛出TypeError。这印证了文档中“只能作用于非 DOM 组件节点”的约束。prop 存在性与类型校验依次检查propName是否为字符串、prop 是否存在使用hasOwn而非in避免继承属性干扰、prop 值是否为函数任一不满足都会抛出带具体信息的异常。柯里化调用返回(...args) {...}闭包这正是“先renderProp(render)再()”这种柯里化调用的来源。调用、包装、返回闭包内部执行propValue(...args)拿到 render prop 返回的元素经adapter.wrap(element)标准化后再通过this.wrap(wrapped, null, this[OPTIONS])生成并返回新的ShallowWrapper。注意第三个参数this[OPTIONS]会把原 wrapper 的渲染选项context、disableLifecycleMethods 等继承给新 wrapper因此在新 wrapper 上继续调用.find()、.equals()、.text()等 API 时行为与父 wrapper 保持一致。与文档中“renderProp本质等价于wrapper.prop(propName)(...args)”的表述相对照可以更精确地说它等价于wrapper.prop(propName)(...args)的“安全增强版”——额外的校验、柯里化与 wrapper 化都是为了让它更适合断言渲染结果。四、约束与错误处理什么时候会抛异常结合源码与 renderProp.jsx 中的测试用例renderProp()会在以下五种场景抛出异常编写测试时务必注意触发场景异常类型与消息当前 wrapper 包装了多个节点single抛出的单节点约束错误当前节点是宿主 DOM 元素如divTypeErrorcan only be called on custom componentspropName不是字符串如省略或传数字TypeErrorpropName must be a string指定的 prop 不存在Errorno prop called “{propName}” foundprop 存在但不是函数TypeErrorexpected prop “{propName}” to contain a function, but it holds “{typeof}”当前 adapter 未实现wrapRangeErroryour adapter does not supportwrap测试套件中对应的断言可以一一找到例如it(throws on a non-string prop name, () { const wrapper Wrap(RendersBar render{() {}} /); expect(() wrapper.renderProp()).to.throw( TypeError, ${WrapperName}::renderProp(): \propName\ must be a string, ); }); it(throws on a missing prop, () { const wrapper Wrap(RendersBar render{() {}} /); expect(() wrapper.renderProp(nope)).to.throw( Error, ${WrapperName}::renderProp(): no prop called “nope“ found, ); }); it(throws on a non-function render prop value, () { const wrapper Wrap(RendersBar render{{}} /); expect(() wrapper.renderProp(render)).to.throw( TypeError, ${WrapperName}::renderProp(): expected prop “render“ to contain a function, but it holds “object“, ); }); it(throws on host elements, () { const wrapper WrapRendered(Div /); expect(wrapper.is(div)).to.equal(true); expect(() wrapper.renderProp(foo)).to.throw(); });这些用例表明错误信息中的WrapperName会随包装器类型变化ShallowWrapper或ReactWrapper而消息内容与源码中的模板完全一致方便测试排错时直接对照源码定位。五、高级用法render prop 返回非节点值当使用 React 16 时render prop 不一定必须返回 React 元素它也可能返回字符串、数字、null、数组乃至布尔值。针对这类“非节点”返回值测试套件专门用describeIf(is( 16), ...)做了分组测试function MyComponent({ val }) { return ComponentWithRenderProp val{val} r{(x) x} /; } function ComponentWithRenderProp({ val, r }) { return r(val); }在此基础上验证了以下返回值均可被renderProp处理字符串renderProp(r)(foo)、renderProp(r)()数字renderProp(r)(42)、renderProp(r)(0)、renderProp(r)(NaN)nullrenderProp(r)(null)数组renderProp(r)([])、renderProp(r)([a])、renderProp(r)([Infinity])falserenderProp(r)(false)。这也解释了为什么renderProp在内部要先经过adapter.wrap(element)再做this.wrap(...)不同 React 版本对非节点返回值的渲染语义不同交由 adapter 统一标准化可以抹平版本差异该分组测试仅在 React ≥ 16 时执行正是这一原因。需要注意的是测试中有一个被it.skip跳过的undefined用例源码注释为FIXME: figure out how to test this reliably说明对undefined返回值的处理在当前版本中并不稳定不建议在业务测试中依赖。六、与相关 API 的对比与组合renderProp并非孤立 API与它相关的还有几个容易混淆的 API选型时可以从“是否要重新渲染为 wrapper”这一维度区分prop(propName)仅返回 prop 的原始值不调用、不包装。renderProp内部正是基于它取出函数再执行的。invoke(propName)同样调用函数 prop 并透传参数但返回的是函数的返回值且会在调用后自动update()根 wrapper适合触发事件回调、验证副作用renderProp则把返回值包装成新的 ShallowWrapper适合断言渲染结构。二者一“值”一“树”用途互补。dive()渲染当前组件节点的内部渲染结果对根组件或单个自定义组件调用常与renderProp搭配先用renderProp拿到 render prop 输出并包装若输出中仍含自定义组件再继续.dive()深入。find(selector)与equals(element)renderProp返回的新 wrapper 与普通 wrapper 无异可以无缝链式调用.find()、.equals()、.text()、.hasClass()等全部 ShallowWrapper API。一个典型的组合用法是const wrapper shallow(App /) .find(Mouse) .renderProp(render)(10, 20) .find(h1) .text(); expect(wrapper).to.equal(The mouse position is (10, 20));七、小结ShallowWrapper.renderProp(propName)(...args)是 Enzyme 中针对 Render Prop 模式的一等公民测试工具本质wrapper.prop(propName)(...args)的安全增强版返回包装了渲染结果的ShallowWrapper约束仅能用于包装单个自定义组件节点的 wrapper且 prop 必须是字符串命名的函数能力参数可空可多支持 React 16 的非节点返回值返回的 wrapper 可继续链式断言实现底层由single 四重校验 adapter.wrapthis.wrap(wrapped, null, this[OPTIONS])构成见 ShallowWrapper.js验证所有边界行为错误消息、非节点返回值、适配器缺失wrap均可在 renderProp.jsx 中找到对应用例。掌握了renderProp你便能在保持浅渲染、不触发真实 DOM 挂载的前提下完整、精确地验证 React 生态中最灵活的渲染复用模式之一。赞分享测试前端【免费下载链接】enzymeJavaScript Testing utilities for React项目地址https://gitcode.com/gh_mirrors/en/enzyme点击查看免费下载相关推荐Enzyme ShallowWrapper.getWrappingComponent() 完全指南浅渲染中操作 Provider 等包裹组件Enzyme ShallowWrapper.getWrappingComponent 完全指南浅渲染中操作 Provider 等包裹组件 导读 .getWra测试前端Enzyme ReactWrapper.renderProp() 完全指南如何测试 React Render Props 渲染结果Enzyme ReactWrapper.renderProp 完全指南如何测试 React Render Props 渲染结果 导读 .renderProp测试前端Enzyme API 指南shallow / mount / render 三种渲染模式的完整解析Enzyme API 指南shallow / mount / render 三种渲染模式的完整解析 导读 本文是 enzymeJavaScript Test测试前端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考