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