Enzyme 完整渲染 #

概述 #

Enzyme 提供两种完整渲染方式:mountrender。它们都能渲染完整的组件树,但各有特点:

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