- 测试
- 前端
【免费下载链接】enzyme
JavaScript Testing utilities for React
导读
React Native 组件的单元测试长期受限于其依赖宿主设备的复杂环境,难以接入 CI。本文以 Enzyme 官方指南为基础,系统讲解如何在 React Native 项目中配置 Enzyme Adapter、加载 JSDOM 模拟 DOM,并借助testID与findWhere等 API 完成组件查找、事件模拟与快照测试。读完本文,你将掌握一套可直接复制到 Jest、Mocha 等测试框架中运行的 React Native 测试方案,并理解其底层原理(Adapter 校验、mount依赖 DOM 的原因、React Navigation 快照失败的规避方法)。
为什么 React Native 组件可以用 Enzyme 测试
自 React Native v0.18 起,React Native 将 React 作为普通依赖引入,而不再维护一个 React 的分支(fork)。这意味着 React 生态中的测试工具——包括 Enzyme 的shallow渲染——可以直接作用于 React Native 组件。
不过,React Native 本身带有大量环境级依赖(原生模块、触摸事件、宿主平台 API 等),这些依赖在没有真实设备(或模拟器)的环境中很难被模拟。因此当你希望在 Travis 等典型 CI 服务器上运行测试套件时,就需要额外补齐两样东西:
- 一个 Enzyme Adapter:让 Enzyme 知道当前被测的是哪个 React 版本、如何遍历其渲染树;
- 一个模拟的 DOM(emulated DOM):供
mount(完整 DOM 渲染)挂载组件使用。
配置 Enzyme Adapter
基础配置代码
在写任何测试之前,必须调用Enzyme.configure注册 Adapter。React Native 专用的 Adapter 仍在社区讨论中(对应 enzyme 仓库 issue #1436),因此在专用 Adapter 出现之前,官方推荐直接使用标准 Web Adapter,例如enzyme-adapter-react-16:
import Adapter from 'enzyme-adapter-react-16'; Enzyme.configure({ adapter: new Adapter() });源码视角:为什么必须配置 Adapter
Enzyme.configure在 packages/enzyme/src/index.js 中被导出为configure,其底层实现位于 packages/enzyme/src/configuration.js:它先将配置与已有配置合并(merge),合并时若传入adapter则会调用validateAdapter做严格校验,最后通过Object.assign写入全局 configuration。
校验逻辑(见 packages/enzyme/src/validateAdapter.js)非常严格:
- 未传入 adapter 时抛出错误,提示必须先调用
Enzyme.configure({ adapter: new Adapter() }); - 传入的是构造函数而非实例(
typeof adapter === 'function')时报错; - 传入的实例未继承自
EnzymeAdapter基类(packages/enzyme/src/EnzymeAdapter.js)时报错。
也就是说,配置必须传入 adapter 实例,且必须与项目 React 版本匹配。在 React Native 项目中选择哪个版本的标准 Adapter,取决于 React Native 内部捆绑的 React 版本(本文以 react-16 对应的enzyme-adapter-react-16为例,该 Adapter 源码见 packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js)。
加载模拟 DOM:JSDOM 的正确姿势
在 React Native 专用 Adapter 出现之前,要使用 Enzyme 的mount(完整 DOM 渲染)就必须先加载一个模拟的 DOM。社区中虽有react-native-mock-renderer等方案,但官方推荐的方案是使用JSDOM——一个完全用 JavaScript 实现的 headless 浏览器,具体做法与纯 Web 环境下的 enzyme + JSDOM 指南(见 docs/guides/jsdom.md)一致。
为什么要模拟 DOM
mount的实现(packages/enzyme/src/mount.js)本质上是new ReactWrapper(node, null, options),而 ReactWrapper 需要把组件真实挂载进 DOM 才能工作。正如 docs/api/mount.md 所述:完整 DOM 渲染要求全局作用域中存在一套“看起来像浏览器”的 DOM API。在 Node 环境里,JSDOM 就是提供这套 API 的最优选择。
关键技术点:在 React 首次 require 之前加载 DOM
官方在 docs/guides/jsdom.md 中强调:最好在 React 被首次 require 之前,就把 document 加载进全局作用域。因为 React 源码在 require 时会读取global.document并假定它是唯一需要关心的 document,之后再“重新加载” document 会导致各种隐性副作用。这也是为什么官方不再推荐旧版describeWithDOMAPI(它在每个测试前动态换 document)的原因。
一个标准的 setup 脚本如下(jsdom v10+API):
/* setup.js */ const { JSDOM } = require('jsdom'); const jsdom = new JSDOM('<!doctype html><html><body></body></html>'); const { window } = jsdom; function copyProps(src, target) { Object.defineProperties(target, { ...Object.getOwnPropertyDescriptors(src), ...Object.getOwnPropertyDescriptors(target), }); } global.window = window; global.document = window.document; global.navigator = { userAgent: 'node.js', }; global.requestAnimationFrame = function (callback) { return setTimeout(callback, 0); }; global.cancelAnimationFrame = function (id) { clearTimeout(id); }; copyProps(window, global);要点说明:
new JSDOM('<!doctype html><html><body></body></html>')创建了一个最小但完整的 HTML 文档;global.window、global.document、global.navigator挂到全局,React 才能正常挂载;copyProps(window, global)用属性描述符把 window 上的所有属性复制到 global,弥补新版 jsdom 不再隐式把属性暴露到全局的差异;requestAnimationFrame/cancelAnimationFrame用定时器兜底,避免测试环境缺少 rAF 报错。
在 Mocha 等框架中,可通过--require让该脚本在测试套件运行前执行:
mocha --require setup.js --recursive path/to/test/dir没有 className 时,用 testID 作为选择器
标准 React 测试中常用.find('.foo')按 className 查找节点,但 React Native 的组件树里没有 DOM className 概念。React Native 为此提供了testIDprop,它可以在 Enzyme 中扮演类似className的选择器角色:
<View key={key} style={styles.todo} testID="todo-item"> <Text testID="todo-title" style={styles.title}>{todo.title}</Text> </View>配合findWhere按 prop 精确查找:
expect(wrapper.findWhere((node) => node.prop('testID') === 'todo-item')).toExist();findWhere(API 文档见 docs/api/ReactWrapper/findWhere.md)会对渲染树中每个节点调用传入的谓词函数,返回所有结果为 true 的节点构成的新 wrapper。其底层实现在 packages/enzyme/src/ReactWrapper.js 与 packages/enzyme/src/ShallowWrapper.js 中通过treeFilter完成全树遍历,因此node.prop('testID')这种按任意 prop 过滤的方式,本质上就是一次全量遍历 + 谓词筛选,可以覆盖 RN 组件树中任意层次的节点。
Jest 默认配置:setupFilesAfterEnv + JSDOM
要把上述环境配置接入测试框架,官方推荐使用独立 setup 脚本,例如通过 Jest 的setupFilesAfterEnv设置加载它。
第一步:配置 jest.config.js
在项目根目录创建或更新jest.config.js,加入setupFilesAfterEnv:
// jest.config.js module.exports = { // Load setup-tests.js before test execution setupFilesAfterEnv: '<rootDir>setup-tests.js', // ... };第二步:编写 setup-tests.js
然后创建或更新setupFilesAfterEnv指定的文件(本例为项目根目录的setup-tests.js),在测试执行前完成两件事:模拟 DOM + 配置 Enzyme:
// setup-tests.js import 'react-native'; import 'jest-enzyme'; import Adapter from 'enzyme-adapter-react-16'; import Enzyme from 'enzyme'; /** * Set up DOM in node.js environment for Enzyme to mount to */ const { JSDOM } = require('jsdom'); const jsdom = new JSDOM('<!doctype html><html><body></body></html>'); const { window } = jsdom; function copyProps(src, target) { Object.defineProperties(target, { ...Object.getOwnPropertyDescriptors(src), ...Object.getOwnPropertyDescriptors(target), }); } global.window = window; global.document = window.document; global.navigator = { userAgent: 'node.js', }; copyProps(window, global); /** * Set up Enzyme to mount to DOM, simulate events, * and inspect the DOM in tests. */ Enzyme.configure({ adapter: new Adapter() });这段脚本包含三层职责:
import 'react-native'提前加载 RN 运行时,并确保它在 JSDOM 全局就绪之后才被求值;import 'jest-enzyme'引入 jest-enzyme 的匹配器,toExist()等断言即来源于此;Enzyme.configure({ adapter: new Adapter() })完成上一节所述的 Adapter 注册。
其他测试库:把 JSDOM 开箱即用地塞进单个测试文件
如果你使用的不是 Jest 的全局环境配置,或者希望只在个别测试文件里启用 DOM,可以采用“按文件启用 jsdom 环境”的方式。同样先写好一个不含 JSDOM 的轻量 setup:
import 'react-native'; import 'jest-enzyme'; import Adapter from 'enzyme-adapter-react-16'; import Enzyme from 'enzyme'; /** * Set up Enzyme to mount to DOM, simulate events, * and inspect the DOM in tests. */ Enzyme.configure({ adapter: new Adapter() });创建独立的测试文件
然后创建一个以enzyme.test.js(或enzyme.test.ts)命名的独立测试文件,例如component.enzyme.test.js:
/** * @jest-environment jsdom */ import React from 'react'; import { mount } from 'enzyme'; import { Text } from '../../../component/text'; describe('Component tested with enzyme', () => { test('App mount with enzyme', () => { const wrapper = mount(<Text />); // other tests operations }); });最关键的一点是确保测试在jestEnvironment为jsdom时运行——最简单的方式就是在文件顶部加一行/* @jest-environment jsdom */注释。这样 JSDOM 环境会在该文件运行时按需建立,mount便有 DOM 可挂载,其他不使用mount的测试文件则保持默认的 node 环境,互不干扰。
做完以上配置,你就可以开始正式编写测试了。
完整实战:Todo 列表的 Enzyme 测试
下面是一个完整的 Todo 列表演示,覆盖了前文所有技巧,并额外展示了 React Native 测试中两个常见的坑:原生组件的事件模拟与快照测试的随机 key 问题。
import React from 'react'; import renderer from 'react-test-renderer'; import { mount, ReactWrapper } from 'enzyme'; import { Provider } from 'mobx-react'; import { Text } from 'native-base'; import { TodoItem } from './todo-item'; import { TodoList } from './todo-list'; import { todoStore } from '../../stores/todo-store'; // React Navigation generates random React keys, which makes // snapshot testing fail. Mock the randomness to keep from failing. jest.mock('react-navigation/src/routers/KeyGenerator', () => ({ generateKey: jest.fn(() => 123), })); describe('todo-list', () => { describe('enzyme tests', () => { it('can add a Todo with Enzyme', () => { const wrapper = mount( <Provider keyLength={0} todoStore={todoStore}> <TodoList /> </Provider>, ); const newTodoText = 'I need to do something...'; const newTodoTextInput = wrapper.find('Input').first(); const addTodoButton = wrapper .find('Button') .findWhere((w) => w.text() === 'Add Todo') .first(); newTodoTextInput.props().onChangeText(newTodoText); // Enzyme usually allows wrapper.simulate() alternatively, but this doesn't support 'press' events. addTodoButton.props().onPress(); // Make sure to call update if external events (e.g. Mobx state changes) // result in updating the component props. wrapper.update(); // You can either check for a testID prop, similar to className in React: expect( wrapper.findWhere((node) => node.prop('testID') === 'todo-item'), ).toExist(); // Or even just find a component itself, if you broke the JSX out into its own component: expect(wrapper.find(TodoItem)).toExist(); // You can even do snapshot testing, // if you pull in enzyme-to-json and configure // it in snapshotSerializers in package.json expect(wrapper.find(TodoList)).toMatchSnapshot(); }); }); });逐段拆解
1. 事件模拟:simulate并不万能
React Native 中没有浏览器事件模型,wrapper.simulate('press')这类调用无法触发 RN 的原生触摸事件。因此示例采用直接调用 props 回调的方式:newTodoTextInput.props().onChangeText(newTodoText)模拟输入,addTodoButton.props().onPress()模拟点击。
simulate的适用场景(API 说明见 docs/api/ReactWrapper/simulate.md)是模拟 DOM 事件并可选传入 mock 事件对象;在 RN 环境中,其限制正如示例注释所写——不支持press等 RN 原生事件。
2. 查找策略:组合使用find与findWhere
wrapper.find('Input').first():按组件名/类型查找;wrapper.find('Button').findWhere((w) => w.text() === 'Add Todo').first():先按类型过滤,再用谓词按文本内容精确定位;wrapper.findWhere((node) => node.prop('testID') === 'todo-item'):按testIDprop 定位,等效于 Web 端按 className 查找;wrapper.find(TodoItem):直接按组件类查找,前提是该子组件已拆分为独立 JSX 组件。
3. 外部状态变更后必须wrapper.update()
MobX 等外部状态库引起的 props 更新不会自动同步到 Enzyme 的组件树快照。update()的作用是重新同步 enzyme 组件树快照与 React 组件树(见 docs/api/ReactWrapper/update.md),所以在触发外部状态变更后、断言之前调用它是必须的。
4. 快照测试:解决随机 key 问题
示例最后用toMatchSnapshot()做快照断言,但 React Navigation 会为路由生成随机 React key,导致快照每次运行都不同、永远失败。解决方案是在测试中 mock 掉react-navigation/src/routers/KeyGenerator,让generateKey恒返回固定值(jest.fn(() => 123)),从而保证快照稳定。
另外需要说明两点:
- 快照断言需配合
enzyme-to-json,并在package.json中配置snapshotSerializers才能输出稳定的 Enzyme 树结构; - 官方指南同时指出:Jest 快照测试虽然可以配合 JSDOM 方案使用,但并不鼓励,且仅支持通过
wrapper.debug()输出调试字符串(见 docs/api/ReactWrapper/debug.md)。
补充注意事项
- 额外的原生组件 mock:根据被测组件依赖的原生模块不同,你可能还需要对原生组件进行额外的 mocking;
- 环境加载顺序:JSDOM 文档必须在 React 首次 require 之前进入全局作用域(见上文“关键技术点”),这是整套方案稳定运行的前提;
- Adapter 版本匹配:Adapter 必须与项目实际使用的 React 版本一致,选错版本会导致渲染树遍历异常(Adapter 校验规则见 packages/enzyme/src/validateAdapter.js);
- 测试间隔离:
mount是真实 DOM 渲染,多个测试共用同一 DOM 会产生相互影响,必要时用.unmount()或.detach()清理(完整 ReactWrapper API 清单见 docs/api/mount.md)。
总结
在 React Native 中接入 Enzyme,核心只需三步:用Enzyme.configure注册与 React 版本匹配的标准 Adapter;用 JSDOM 在测试环境建立模拟 DOM(并确保其在 React 加载前就绪);通过testID+findWhere弥补 className 缺失,以 props 回调代替simulate完成事件驱动。配合 Jest 的setupFilesAfterEnv与按文件启用的@jest-environment jsdom,这套方案可以稳定地跑在 Travis 等 CI 服务器上,让 React Native 组件测试拥有与 Web 侧一致的 Enzyme 体验。
- 测试
- 前端
【免费下载链接】enzyme
JavaScript Testing utilities for React
相关推荐
T3 Code HTTP边界实现详解:http、ws、CORS与响应错误防护如何配合的完整指南
T3 Code HTTP边界实现详解:http、ws、CORS与响应错误防护如何配合的完整指南 T3 Code 是一款面向开发者的 AI 编程协作工具,它的本地
AI Agent代码智能体后端前端移动开发桌面应用Enzyme在React Native组件测试中的实践指南
Enzyme在React Native组件测试中的实践指南 前言 在React Native应用开发中,组件测试是保证应用质量的重要环节。Enzyme作为Rea
测试前端Tack高级配置:深入理解Terraform模块化架构设计
Tack高级配置:深入理解Terraform模块化架构设计 Tack作为基于Terraform的Kubernetes集群部署工具,其模块化架构设计是实现灵活配置
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考