Enzyme 浅层渲染 #
什么是浅层渲染? #
浅层渲染(Shallow Rendering)是 Enzyme 提供的一种渲染方式,它只渲染组件的第一层,不渲染子组件。这种方式非常适合单元测试,因为它可以隔离测试目标组件,不受子组件实现的影响。
渲染层次对比 #
text
┌─────────────────────────────────────────────────────────────┐
│ 完整渲染 (mount) │
├─────────────────────────────────────────────────────────────┤
│ ParentComponent │
│ └── ChildComponent │
│ └── GrandChildComponent │
│ └── GreatGrandChildComponent │
│ 所有层级都被渲染 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 浅层渲染 (shallow) │
├─────────────────────────────────────────────────────────────┤
│ ParentComponent │
│ └── <ChildComponent /> (未渲染,显示为占位符) │
│ 只渲染第一层,子组件保持未渲染状态 │
└─────────────────────────────────────────────────────────────┘
基本用法 #
引入 shallow #
javascript
import { shallow } from 'enzyme';
渲染组件 #
javascript
import React from 'react';
import { shallow } from 'enzyme';
import MyComponent from './MyComponent';
describe('MyComponent', () => {
it('renders correctly', () => {
const wrapper = shallow(<MyComponent />);
expect(wrapper.exists()).toBe(true);
});
});
ShallowWrapper 对象 #
shallow() 返回一个 ShallowWrapper 对象,提供了丰富的方法来操作和断言组件:
javascript
const wrapper = shallow(<MyComponent />);
// ShallowWrapper 的核心方法
wrapper.find(selector); // 查找元素
wrapper.filter(selector); // 过滤元素
wrapper.text(); // 获取文本内容
wrapper.html(); // 获取 HTML
wrapper.debug(); // 获取调试信息
浅层渲染的优势 #
1. 测试隔离 #
javascript
// ParentComponent.js
function ParentComponent() {
return (
<div className="parent">
<ChildComponent />
</div>
);
}
// ParentComponent.test.js
describe('ParentComponent', () => {
it('renders child component', () => {
const wrapper = shallow(<ParentComponent />);
// ChildComponent 不会被渲染
// 测试不受 ChildComponent 实现的影响
expect(wrapper.find(ChildComponent).length).toBe(1);
expect(wrapper.find('.parent').length).toBe(1);
});
});
2. 执行速度快 #
javascript
// 浅层渲染不需要渲染子组件
// 测试执行更快
describe('Performance comparison', () => {
it('shallow is faster than mount', () => {
const startShallow = Date.now();
shallow(<ComplexComponent />);
const shallowTime = Date.now() - startShallow;
// shallow 通常比 mount 快很多
console.log(`Shallow: ${shallowTime}ms`);
});
});
3. 不依赖子组件 #
javascript
// 即使子组件有错误,父组件测试仍然通过
function BuggyChild() {
throw new Error('I am buggy!');
}
function Parent() {
return (
<div>
<BuggyChild />
</div>
);
}
describe('Parent', () => {
it('renders without crashing', () => {
// shallow 不会执行 BuggyChild,所以测试通过
const wrapper = shallow(<Parent />);
expect(wrapper.find('div').length).toBe(1);
});
});
核心 API #
查找元素 #
CSS 选择器 #
javascript
const wrapper = shallow(<MyComponent />);
// 类选择器
wrapper.find('.my-class');
// ID 选择器
wrapper.find('#my-id');
// 标签选择器
wrapper.find('div');
wrapper.find('button');
// 属性选择器
wrapper.find('[type="submit"]');
wrapper.find('[data-testid="submit-button"]');
// 组合选择器
wrapper.find('div.container.active');
wrapper.find('button[type="submit"]');
组件选择器 #
javascript
import ChildComponent from './ChildComponent';
const wrapper = shallow(<ParentComponent />);
// 查找子组件
wrapper.find(ChildComponent);
// 使用组件名称
wrapper.find('ChildComponent');
复合选择器 #
javascript
const wrapper = shallow(<MyComponent />);
// 查找多个元素
const buttons = wrapper.find('button');
// 链式查找
const submitButton = wrapper.find('form').find('button[type="submit"]');
// 条件查找
const activeItems = wrapper.find('.item').filter('.active');
遍历元素 #
javascript
const wrapper = shallow(<List items={['a', 'b', 'c']} />);
// 获取所有列表项
const items = wrapper.find('li');
// 遍历
items.forEach((node, index) => {
console.log(`Item ${index}: ${node.text()}`);
});
// 映射
const texts = items.map(node => node.text());
// 获取特定索引
const firstItem = items.at(0);
const lastItem = items.at(items.length - 1);
// first() 和 last()
const first = items.first();
const last = items.last();
过滤元素 #
javascript
const wrapper = shallow(<MyComponent />);
// filter - 保留匹配的元素
wrapper.find('li').filter('.active');
// filterWhere - 使用函数过滤
wrapper.find('li').filterWhere(node => node.text().length > 5);
// not - 排除匹配的元素
wrapper.find('li').not('.disabled');
检查元素 #
javascript
const wrapper = shallow(<MyComponent />);
// 检查是否存在
wrapper.exists(); // 组件是否存在
wrapper.find('.btn').exists(); // 元素是否存在
// 检查类名
wrapper.hasClass('active');
wrapper.find('div').hasClass('container');
// 检查是否包含
wrapper.contains(<span>Hello</span>);
wrapper.containsMatchingElement(<span>Hello</span>);
// 检查文本
wrapper.text(); // 获取文本
wrapper.find('.title').text(); // 获取特定元素文本
// 检查 HTML
wrapper.html(); // 获取 HTML
wrapper.render().html(); // 获取静态 HTML
实用示例 #
测试简单组件 #
javascript
// Button.js
function Button({ text, onClick, disabled }) {
return (
<button
className="btn"
onClick={onClick}
disabled={disabled}
>
{text}
</button>
);
}
// Button.test.js
describe('Button', () => {
it('renders with text', () => {
const wrapper = shallow(<Button text="Click me" />);
expect(wrapper.text()).toBe('Click me');
});
it('has correct className', () => {
const wrapper = shallow(<Button text="Click" />);
expect(wrapper.hasClass('btn')).toBe(true);
});
it('can be disabled', () => {
const wrapper = shallow(<Button text="Click" disabled />);
expect(wrapper.prop('disabled')).toBe(true);
});
it('handles click', () => {
const onClick = jest.fn();
const wrapper = shallow(<Button text="Click" onClick={onClick} />);
wrapper.simulate('click');
expect(onClick).toHaveBeenCalled();
});
});
测试条件渲染 #
javascript
// Greeting.js
function Greeting({ isLoggedIn, username }) {
return (
<div className="greeting">
{isLoggedIn ? (
<span className="welcome">Welcome, {username}!</span>
) : (
<span className="login-prompt">Please log in</span>
)}
</div>
);
}
// Greeting.test.js
describe('Greeting', () => {
it('shows welcome message when logged in', () => {
const wrapper = shallow(<Greeting isLoggedIn={true} username="John" />);
expect(wrapper.find('.welcome').exists()).toBe(true);
expect(wrapper.find('.welcome').text()).toBe('Welcome, John!');
expect(wrapper.find('.login-prompt').exists()).toBe(false);
});
it('shows login prompt when not logged in', () => {
const wrapper = shallow(<Greeting isLoggedIn={false} />);
expect(wrapper.find('.login-prompt').exists()).toBe(true);
expect(wrapper.find('.login-prompt').text()).toBe('Please log in');
expect(wrapper.find('.welcome').exists()).toBe(false);
});
});
测试列表渲染 #
javascript
// TodoList.js
function TodoList({ items }) {
return (
<ul className="todo-list">
{items.map((item, index) => (
<li key={item.id} className="todo-item">
<span className="todo-text">{item.text}</span>
<button
className="delete-btn"
onClick={() => item.onDelete(item.id)}
>
Delete
</button>
</li>
))}
</ul>
);
}
// TodoList.test.js
describe('TodoList', () => {
const mockItems = [
{ id: 1, text: 'Task 1', onDelete: jest.fn() },
{ id: 2, text: 'Task 2', onDelete: jest.fn() },
{ id: 3, text: 'Task 3', onDelete: jest.fn() }
];
it('renders all items', () => {
const wrapper = shallow(<TodoList items={mockItems} />);
expect(wrapper.find('.todo-item').length).toBe(3);
});
it('renders correct text for each item', () => {
const wrapper = shallow(<TodoList items={mockItems} />);
const texts = wrapper.find('.todo-text').map(node => node.text());
expect(texts).toEqual(['Task 1', 'Task 2', 'Task 3']);
});
it('handles delete click', () => {
const wrapper = shallow(<TodoList items={mockItems} />);
wrapper.find('.delete-btn').at(0).simulate('click');
expect(mockItems[0].onDelete).toHaveBeenCalledWith(1);
});
});
测试表单组件 #
javascript
// LoginForm.js
function LoginForm({ onSubmit }) {
const [formData, setFormData] = useState({ username: '', password: '' });
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(formData);
};
return (
<form className="login-form" onSubmit={handleSubmit}>
<input
name="username"
type="text"
value={formData.username}
onChange={handleChange}
placeholder="Username"
/>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}
// LoginForm.test.js
describe('LoginForm', () => {
it('renders form fields', () => {
const wrapper = shallow(<LoginForm onSubmit={jest.fn()} />);
expect(wrapper.find('input[name="username"]').length).toBe(1);
expect(wrapper.find('input[name="password"]').length).toBe(1);
expect(wrapper.find('button[type="submit"]').length).toBe(1);
});
it('updates state on input change', () => {
const wrapper = shallow(<LoginForm onSubmit={jest.fn()} />);
wrapper.find('input[name="username"]').simulate('change', {
target: { name: 'username', value: 'testuser' }
});
expect(wrapper.state('formData').username).toBe('testuser');
});
it('calls onSubmit with form data', () => {
const mockSubmit = jest.fn();
const wrapper = shallow(<LoginForm onSubmit={mockSubmit} />);
wrapper.setState({
formData: { username: 'testuser', password: 'testpass' }
});
wrapper.find('form').simulate('submit', { preventDefault: jest.fn() });
expect(mockSubmit).toHaveBeenCalledWith({
username: 'testuser',
password: 'testpass'
});
});
});
测试子组件交互 #
javascript
// ParentComponent.js
function ParentComponent() {
const [count, setCount] = useState(0);
return (
<div>
<span className="count">{count}</span>
<ChildComponent onIncrement={() => setCount(count + 1)} />
</div>
);
}
// ParentComponent.test.js
describe('ParentComponent', () => {
it('passes correct props to child', () => {
const wrapper = shallow(<ParentComponent />);
const child = wrapper.find(ChildComponent);
expect(child.prop('onIncrement')).toBeDefined();
});
it('updates count when child calls onIncrement', () => {
const wrapper = shallow(<ParentComponent />);
// 获取传递给子组件的回调并调用
const onIncrement = wrapper.find(ChildComponent).prop('onIncrement');
onIncrement();
expect(wrapper.find('.count').text()).toBe('1');
});
});
高级技巧 #
使用 dive() 深入子组件 #
javascript
// 当需要测试子组件但不想使用 mount 时
function Parent() {
return (
<div>
<Child value="test" />
</div>
);
}
function Child({ value }) {
return <span>{value}</span>;
}
describe('Parent with dive', () => {
it('can test child content using dive', () => {
const wrapper = shallow(<Parent />);
// Child 组件未渲染
expect(wrapper.find(Child).length).toBe(1);
expect(wrapper.find('span').length).toBe(0);
// 使用 dive 渲染子组件
const childWrapper = wrapper.find(Child).dive();
expect(childWrapper.find('span').length).toBe(1);
expect(childWrapper.text()).toBe('test');
});
});
使用 shallow 的 options 参数 #
javascript
// 禁用生命周期方法
const wrapper = shallow(<MyComponent />, {
disableLifecycleMethods: true
});
// 自定义 context
const wrapper = shallow(<MyComponent />, {
context: { theme: 'dark' }
});
快照测试 #
javascript
import React from 'react';
import { shallow } from 'enzyme';
import MyComponent from './MyComponent';
describe('MyComponent snapshots', () => {
it('matches snapshot', () => {
const wrapper = shallow(<MyComponent title="Test" />);
expect(wrapper).toMatchSnapshot();
});
it('matches snapshot with different props', () => {
const wrapper = shallow(<MyComponent title="Another Test" active />);
expect(wrapper).toMatchSnapshot();
});
});
调试技巧 #
javascript
// 使用 debug() 查看组件结构
const wrapper = shallow(<MyComponent />);
console.log(wrapper.debug());
// 输出示例:
// <div className="container">
// <h1>Title</h1>
// <ChildComponent value="test" />
// </div>
// 使用 html() 查看渲染后的 HTML
console.log(wrapper.html());
// 查看特定元素
console.log(wrapper.find('.container').debug());
浅层渲染的局限性 #
无法测试的行为 #
javascript
// 以下场景 shallow 无法满足,需要使用 mount:
// 1. DOM 事件传播
wrapper.find('.parent').simulate('click'); // 子元素不会收到事件
// 2. Context 传播
// shallow 不会将 Context 传递给子组件
// 3. Refs
// shallow 不支持 refs
// 4. 完整的生命周期
// 某些生命周期方法在 shallow 中不会触发
何时使用 mount #
javascript
// 需要使用 mount 的场景:
// 1. 测试 DOM 交互
mount(<Component />).find('input').simulate('change', ...);
// 2. 测试 Context
mount(<Provider><Component /></Provider>);
// 3. 测试 Refs
mount(<Component />).instance().myRef.current;
// 4. 测试完整的组件树
mount(<App />).find(DeepChild).simulate('click');
最佳实践 #
1. 优先使用 shallow #
javascript
// ✅ 好的做法 - 单元测试使用 shallow
describe('Button', () => {
it('renders correctly', () => {
const wrapper = shallow(<Button />);
expect(wrapper.exists()).toBe(true);
});
});
// ❌ 避免 - 简单组件使用 mount
describe('Button', () => {
it('renders correctly', () => {
const wrapper = mount(<Button />); // 不必要
expect(wrapper.exists()).toBe(true);
wrapper.unmount();
});
});
2. 合理使用选择器 #
javascript
// ✅ 好的做法 - 使用语义化的选择器
wrapper.find('[data-testid="submit-button"]');
wrapper.find('.submit-button');
// ❌ 避免 - 脆弱的选择器
wrapper.find('div > div > button'); // 结构变化就会失败
3. 测试行为而非实现 #
javascript
// ✅ 好的做法 - 测试用户可见的行为
it('shows error message when input is empty', () => {
const wrapper = shallow(<Form />);
wrapper.find('button').simulate('click');
expect(wrapper.find('.error').text()).toBe('Field is required');
});
// ❌ 避免 - 过度测试实现细节
it('sets error state to true', () => {
const wrapper = shallow(<Form />);
wrapper.find('button').simulate('click');
expect(wrapper.state('hasError')).toBe(true); // 实现细节
});
下一步 #
现在你已经掌握了浅层渲染的使用方法,接下来学习 完整渲染 了解如何进行集成测试!
最后更新:2026-03-28