Enzyme 完整渲染 #
概述 #
Enzyme 提供两种完整渲染方式:mount 和 render。它们都能渲染完整的组件树,但各有特点:
text
┌─────────────────────────────────────────────────────────────┐
│ 完整渲染方式对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ mount (ReactWrapper) │
│ ├── 渲染完整组件树 │
│ ├── 使用真实 DOM (jsdom) │
│ ├── 支持所有 React 特性 │
│ ├── 支持生命周期方法 │
│ └── 适合:集成测试、DOM 交互、生命周期测试 │
│ │
│ render (CheerioWrapper) │
│ ├── 渲染完整组件树 │
│ ├── 生成静态 HTML │
│ ├── 使用 Cheerio 解析 │
│ ├── 不支持 React 特性 │
│ └── 适合:HTML 结构验证、快照测试 │
│ │
└─────────────────────────────────────────────────────────────┘
Mount 完整渲染 #
什么是 Mount? #
mount 将组件渲染到真实的 DOM 节点中(在测试环境中使用 jsdom 模拟),渲染完整的组件树,支持所有 React 特性。
基本用法 #
javascript
import { mount } from 'enzyme';
describe('MyComponent', () => {
it('renders with mount', () => {
const wrapper = mount(<MyComponent />);
expect(wrapper.exists()).toBe(true);
wrapper.unmount(); // 清理
});
});
ReactWrapper 对象 #
mount() 返回一个 ReactWrapper 对象:
javascript
const wrapper = mount(<MyComponent />);
// ReactWrapper 特有方法
wrapper.mount(); // 重新挂载
wrapper.unmount(); // 卸载组件
wrapper.ref(name); // 获取 ref
wrapper.instance(); // 获取组件实例
wrapper.getDOMNode(); // 获取 DOM 节点
Mount 的优势 #
1. 完整的组件树渲染 #
javascript
function Parent() {
return (
<div className="parent">
<Child>
<GrandChild />
</Child>
</div>
);
}
describe('Parent', () => {
it('renders all descendants', () => {
const wrapper = mount(<Parent />);
// 所有子组件都被渲染
expect(wrapper.find('.parent').length).toBe(1);
expect(wrapper.find(Child).length).toBe(1);
expect(wrapper.find(GrandChild).length).toBe(1);
wrapper.unmount();
});
});
2. 真实的 DOM 交互 #
javascript
function InputForm() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
describe('InputForm', () => {
it('handles real DOM events', () => {
const wrapper = mount(<InputForm />);
const input = wrapper.find('input');
// 模拟真实的 DOM 事件
input.simulate('change', { target: { value: 'test' } });
expect(wrapper.find('input').prop('value')).toBe('test');
wrapper.unmount();
});
});
3. 生命周期方法支持 #
javascript
class LifecycleComponent extends React.Component {
componentDidMount() {
this.props.onMount();
}
componentDidUpdate() {
this.props.onUpdate();
}
componentWillUnmount() {
this.props.onUnmount();
}
render() {
return <div>{this.props.value}</div>;
}
}
describe('LifecycleComponent', () => {
it('calls lifecycle methods', () => {
const onMount = jest.fn();
const onUpdate = jest.fn();
const onUnmount = jest.fn();
const wrapper = mount(
<LifecycleComponent
value="initial"
onMount={onMount}
onUpdate={onUpdate}
onUnmount={onUnmount}
/>
);
expect(onMount).toHaveBeenCalled();
wrapper.setProps({ value: 'updated' });
expect(onUpdate).toHaveBeenCalled();
wrapper.unmount();
expect(onUnmount).toHaveBeenCalled();
});
});
4. Refs 支持 #
javascript
class RefComponent extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
focus() {
this.inputRef.current.focus();
}
render() {
return <input ref={this.inputRef} />;
}
}
describe('RefComponent', () => {
it('can access refs', () => {
const wrapper = mount(<RefComponent />);
// 通过实例访问 ref
const instance = wrapper.instance();
expect(instance.inputRef.current).toBeDefined();
wrapper.unmount();
});
});
Mount 实用示例 #
测试事件传播 #
javascript
function EventPropagation() {
const [clicked, setClicked] = useState('');
return (
<div
className="parent"
onClick={() => setClicked('parent')}
>
<button
className="child"
onClick={(e) => {
e.stopPropagation();
setClicked('child');
}}
>
Click
</button>
</div>
);
}
describe('EventPropagation', () => {
it('handles event propagation', () => {
const wrapper = mount(<EventPropagation />);
// 点击子元素
wrapper.find('button').simulate('click');
expect(wrapper.state('clicked')).toBe('child');
// 点击父元素
wrapper.find('.parent').simulate('click');
expect(wrapper.state('clicked')).toBe('parent');
wrapper.unmount();
});
});
测试 Context #
javascript
const ThemeContext = React.createContext('light');
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={`btn-${theme}`}>{theme}</button>;
}
function App() {
return (
<ThemeContext.Provider value="dark">
<ThemedButton />
</ThemeContext.Provider>
);
}
describe('Context', () => {
it('receives context value', () => {
const wrapper = mount(<App />);
expect(wrapper.find('button').hasClass('btn-dark')).toBe(true);
expect(wrapper.find('button').text()).toBe('dark');
wrapper.unmount();
});
});
测试 Portals #
javascript
function Modal({ children }) {
return ReactDOM.createPortal(
<div className="modal">{children}</div>,
document.body
);
}
describe('Modal', () => {
it('renders portal to body', () => {
const wrapper = mount(<Modal>Modal Content</Modal>);
// Portal 内容渲染到 body
expect(document.body.querySelector('.modal')).toBeTruthy();
expect(document.body.textContent).toContain('Modal Content');
wrapper.unmount();
});
});
测试异步更新 #
javascript
function AsyncComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
if (!data) return <div>Loading...</div>;
return <div>{data}</div>;
}
describe('AsyncComponent', () => {
it('handles async updates', async () => {
const wrapper = mount(<AsyncComponent />);
expect(wrapper.text()).toBe('Loading...');
// 等待异步更新
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});
wrapper.update();
expect(wrapper.text()).toBe('some data');
wrapper.unmount();
});
});
Render 静态渲染 #
什么是 Render? #
render 将组件渲染为静态 HTML,使用 Cheerio 库解析,返回一个 CheerioWrapper 对象。
基本用法 #
javascript
import { render } from 'enzyme';
describe('MyComponent', () => {
it('renders static HTML', () => {
const wrapper = render(<MyComponent />);
expect(wrapper.find('.container').length).toBe(1);
});
});
CheerioWrapper 对象 #
javascript
const wrapper = render(<MyComponent />);
// Cheerio API 方法
wrapper.find(selector); // 查找元素
wrapper.text(); // 获取文本
wrapper.html(); // 获取 HTML
wrapper.attr(name); // 获取属性
wrapper.hasClass(name); // 检查类名
wrapper.val(); // 获取表单值
Render 的特点 #
1. 静态 HTML 输出 #
javascript
function Button({ text, onClick }) {
return (
<button className="btn" onClick={onClick}>
{text}
</button>
);
}
describe('Button', () => {
it('renders correct HTML', () => {
const wrapper = render(<Button text="Click me" />);
// 获取 HTML 字符串
expect(wrapper.html()).toBe('<button class="btn">Click me</button>');
// 注意:onClick 不会出现在 HTML 中
});
});
2. Cheerio 选择器 #
javascript
function List({ items }) {
return (
<ul className="list">
{items.map(item => (
<li key={item.id} data-id={item.id}>
{item.text}
</li>
))}
</ul>
);
}
describe('List', () => {
it('renders list items', () => {
const items = [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' }
];
const wrapper = render(<List items={items} />);
// Cheerio 选择器
expect(wrapper.find('li').length).toBe(2);
expect(wrapper.find('li').first().text()).toBe('Item 1');
expect(wrapper.find('li[data-id="1"]').length).toBe(1);
});
});
3. HTML 结构验证 #
javascript
function Card({ title, content }) {
return (
<article className="card">
<header>
<h2>{title}</h2>
</header>
<section className="content">
<p>{content}</p>
</section>
</article>
);
}
describe('Card HTML structure', () => {
it('has correct structure', () => {
const wrapper = render(
<Card title="Title" content="Content" />
);
// 验证结构
expect(wrapper.find('article.card').length).toBe(1);
expect(wrapper.find('header h2').text()).toBe('Title');
expect(wrapper.find('section.content p').text()).toBe('Content');
});
});
Render 实用示例 #
快照测试 #
javascript
describe('Snapshots', () => {
it('matches snapshot', () => {
const wrapper = render(<MyComponent />);
expect(wrapper.html()).toMatchSnapshot();
});
});
SEO 关键内容验证 #
javascript
function BlogPost({ title, author, content }) {
return (
<article>
<h1 className="title">{title}</h1>
<meta name="author" content={author} />
<div className="content">{content}</div>
</article>
);
}
describe('BlogPost SEO', () => {
it('contains SEO elements', () => {
const wrapper = render(
<BlogPost
title="React Testing"
author="John"
content="Content..."
/>
);
expect(wrapper.find('h1.title').text()).toBe('React Testing');
expect(wrapper.find('meta[name="author"]').attr('content')).toBe('John');
});
});
服务端渲染验证 #
javascript
function SSRComponent() {
return (
<div>
<h1>Server Side Rendered</h1>
<script dangerouslySetInnerHTML={{ __html: 'window.data = {}' }} />
</div>
);
}
describe('SSR output', () => {
it('renders correct HTML for SSR', () => {
const wrapper = render(<SSRComponent />);
const html = wrapper.html();
expect(html).toContain('<h1>Server Side Rendered</h1>');
expect(html).toContain('window.data = {}');
});
});
Mount vs Render 对比 #
功能对比 #
| 特性 | mount | render |
|---|---|---|
| 完整渲染 | ✅ | ✅ |
| DOM 交互 | ✅ | ❌ |
| 生命周期 | ✅ | ❌ |
| Refs | ✅ | ❌ |
| Context | ✅ | ❌ |
| 状态访问 | ✅ | ❌ |
| 事件模拟 | ✅ | ❌ |
| 执行速度 | 较慢 | 较快 |
| 内存占用 | 较高 | 较低 |
使用场景 #
javascript
// 使用 mount 的场景
// 1. 需要测试 DOM 交互
mount(<Component />).find('button').simulate('click');
// 2. 需要测试生命周期
mount(<Component />).setProps({ value: 'new' });
// 3. 需要访问组件实例
mount(<Component />).instance().myMethod();
// 4. 需要测试 Context
mount(<Provider><Component /></Provider>);
// 5. 需要测试 Refs
mount(<Component />).instance().inputRef.current.focus();
// 使用 render 的场景
// 1. 只需要验证 HTML 结构
render(<Component />).find('.class').length;
// 2. 快照测试
expect(render(<Component />).html()).toMatchSnapshot();
// 3. SEO 内容验证
render(<Component />).find('meta[name="description"]');
// 4. 服务端渲染输出验证
render(<Component />).html();
性能优化 #
Mount 性能考虑 #
javascript
// ❌ 不好的做法 - 不清理会导致内存泄漏
describe('Bad', () => {
it('leaks memory', () => {
mount(<Component />);
// 没有 unmount
});
});
// ✅ 好的做法 - 始终清理
describe('Good', () => {
it('cleans up properly', () => {
const wrapper = mount(<Component />);
// ... 测试代码
wrapper.unmount();
});
});
// ✅ 使用 afterEach 自动清理
describe('With cleanup', () => {
let wrapper;
afterEach(() => {
if (wrapper) {
wrapper.unmount();
wrapper = null;
}
});
it('test 1', () => {
wrapper = mount(<Component />);
});
});
选择合适的渲染方式 #
javascript
// ✅ 简单组件用 shallow
describe('Simple component', () => {
it('renders', () => {
const wrapper = shallow(<Button />);
expect(wrapper.exists()).toBe(true);
});
});
// ✅ HTML 结构验证用 render
describe('HTML structure', () => {
it('has correct structure', () => {
const wrapper = render(<Card />);
expect(wrapper.find('.card').length).toBe(1);
});
});
// ✅ 需要交互时才用 mount
describe('Interaction', () => {
it('handles click', () => {
const wrapper = mount(<InteractiveComponent />);
wrapper.find('button').simulate('click');
wrapper.unmount();
});
});
常见问题解决 #
问题 1:组件未清理 #
javascript
// 问题:控制台警告内存泄漏
// Warning: Can't perform a React state update on an unmounted component.
// 解决方案:使用 unmount
it('cleans up properly', () => {
const wrapper = mount(<Component />);
// 测试代码
wrapper.unmount();
});
问题 2:异步更新未生效 #
javascript
// 问题:状态更新后断言失败
it('updates async', () => {
const wrapper = mount(<AsyncComponent />);
wrapper.find('button').simulate('click');
// 此时状态可能还没更新
expect(wrapper.state('data')).toBe('loaded'); // 可能失败
});
// 解决方案:等待更新
it('updates async correctly', async () => {
const wrapper = mount(<AsyncComponent />);
wrapper.find('button').simulate('click');
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});
wrapper.update();
expect(wrapper.state('data')).toBe('loaded');
wrapper.unmount();
});
问题 3:Portal 测试问题 #
javascript
// 问题:Portal 内容找不到
it('finds portal content', () => {
const wrapper = mount(<Modal>Content</Modal>);
expect(wrapper.find('.modal').length).toBe(1); // 失败
});
// 解决方案:在 document.body 中查找
it('finds portal content correctly', () => {
const wrapper = mount(<Modal>Content</Modal>);
expect(document.body.querySelector('.modal')).toBeTruthy();
wrapper.unmount();
});
最佳实践 #
1. 选择正确的渲染方式 #
javascript
// 决策树
function chooseRenderMethod(testType) {
if (testType === 'unit') return 'shallow';
if (testType === 'html-structure') return 'render';
if (testType === 'integration') return 'mount';
if (testType === 'dom-interaction') return 'mount';
if (testType === 'lifecycle') return 'mount';
return 'shallow'; // 默认
}
2. 清理资源 #
javascript
describe('Component tests', () => {
let wrapper;
afterEach(() => {
if (wrapper) {
wrapper.unmount();
wrapper = null;
}
});
it('test case', () => {
wrapper = mount(<Component />);
// 测试代码
});
});
3. 使用辅助函数 #
javascript
// 测试辅助函数
function mountWithCleanup(component) {
let wrapper;
beforeEach(() => {
wrapper = mount(component);
});
afterEach(() => {
wrapper.unmount();
});
return () => wrapper;
}
// 使用
describe('MyComponent', () => {
const getWrapper = mountWithCleanup(<MyComponent />);
it('test', () => {
const wrapper = getWrapper();
// 测试代码
});
});
下一步 #
现在你已经掌握了完整渲染的使用方法,接下来学习 选择器 了解更多元素查找技巧!
最后更新:2026-03-28