- 测试
- 前端
【免费下载链接】enzyme
JavaScript Testing utilities for React
导读
在 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的那个 prop(render prop)被以args作为参数调用后所返回的 React 节点”。
从参数与返回值角度看:
| 项目 | 说明 |
|---|---|
propName(String) | 要调用的 render prop 的属性名,必须是字符串 |
...args(Array<Any>) | 传递给 render prop 函数的实参列表,可以为空、单个或多个 |
| 返回值 | ShallowWrapper,包装了 render prop 返回的节点 |
官方文档给出了一句非常精辟的等价表述:“This essentially callswrapper.prop(propName)(...args).”也就是说,renderProp本质上等价于先通过prop()取出该函数 prop,再以args直接调用它。两者的差异在于:renderProp额外做了包装与类型校验(详见第三节源码解析),并把调用结果封装成可继续链式操作的 wrapper,而不是返回裸的 React 元素。
需要特别强调的是使用前提:renderProp()只能被调用在包装了单个、非 DOM(custom 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 prop
const wrapper = shallow(<App />) .find(Mouse) .renderProp('render')(); expect(wrapper.equals(<h1>The mouse position is 0, 0</h1>)).to.equal(true);这段测试的调用链值得拆解:
shallow(<App />)得到根 wrapper;.find(Mouse)定位到被渲染出来的Mouse节点(此时 wrapper 内只有一个自定义组件节点,满足调用前提);.renderProp('render')()取出render这个函数 prop,以零参数调用它,得到<h1>The mouse position is (0, 0)</h1>元素,并包装成新的 wrapper;.equals(...)断言新 wrapper 与期望元素结构完全相等。
由于App传入的 render 函数带有默认值x = 0, y = 0,所以即使Mouse的state尚未被鼠标事件更新,无参调用也能渲染出确定内容。
用例二:带多个参数调用 render prop
const wrapper = shallow(<App />) .find(Mouse) .renderProp('render')(10, 20); expect(wrapper.equals(<h1>The 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 元素(如<div>) | TypeError:can only be called on custom components |
propName不是字符串(如省略或传数字) | TypeError:`propName` must be a string |
| 指定的 prop 不存在 | Error:no prop called “{propName}” found |
| prop 存在但不是函数 | TypeError:expected prop “{propName}” to contain a function, but it holds “{typeof}” |
当前 adapter 未实现wrap | RangeError:your 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); - null:
renderProp('r')(null); - 数组:
renderProp('r')([])、renderProp('r')(['a'])、renderProp('r')([Infinity]); - false:
renderProp('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.wrap+this.wrap(wrapped, null, this[OPTIONS])构成,见 ShallowWrapper.js; - 验证:所有边界行为(错误消息、非节点返回值、适配器缺失
wrap)均可在 renderProp.jsx 中找到对应用例。
掌握了renderProp,你便能在保持浅渲染、不触发真实 DOM 挂载的前提下,完整、精确地验证 React 生态中最灵活的渲染复用模式之一。
- 测试
- 前端
【免费下载链接】enzyme
JavaScript Testing utilities for React
相关推荐
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 三种渲染模式的完整解析 导读 本文是 enzyme(JavaScript Test
测试前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考