Enzyme 状态与属性测试 #
概述 #
Enzyme 提供了直接访问和修改组件 state 和 props 的能力,这是它区别于 React Testing Library 的重要特性。这种能力让单元测试更加灵活,但也需要谨慎使用。
State vs Props #
text
┌─────────────────────────────────────────────────────────────┐
│ State 与 Props 对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Props (属性) │
│ ├── 从父组件传入 │
│ ├── 只读,组件内部不应修改 │
│ ├── 用于组件间通信 │
│ └── 测试方法: prop(), props(), setProps() │
│ │
│ State (状态) │
│ ├── 组件内部管理 │
│ ├── 可变,通过 setState 更新 │
│ ├── 用于组件内部状态 │
│ └── 测试方法: state(), setState() │
│ │
└─────────────────────────────────────────────────────────────┘
Props 测试 #
读取 Props #
prop() 方法 #
获取单个属性值:
javascript
function Button({ text, disabled, onClick }) {
return (
<button disabled={disabled} onClick={onClick}>
{text}
</button>
);
}
describe('Button props', () => {
it('receives correct props', () => {
const mockClick = jest.fn();
const wrapper = shallow(
<Button text="Click me" disabled={true} onClick={mockClick} />
);
// 获取单个 prop
expect(wrapper.prop('text')).toBe('Click me');
expect(wrapper.prop('disabled')).toBe(true);
expect(wrapper.prop('onClick')).toBe(mockClick);
});
});
props() 方法 #
获取所有属性:
javascript
describe('Button props', () => {
it('receives all props', () => {
const mockClick = jest.fn();
const wrapper = shallow(
<Button text="Click me" disabled={true} onClick={mockClick} />
);
// 获取所有 props
const allProps = wrapper.props();
expect(allProps).toEqual({
disabled: true,
onClick: mockClick,
children: 'Click me'
});
});
});
设置 Props #
setProps() 方法 #
javascript
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
describe('Counter setProps', () => {
it('updates when props change', () => {
const wrapper = shallow(<Counter initialCount={0} />);
expect(wrapper.find('span').text()).toBe('0');
// 更新 props
wrapper.setProps({ initialCount: 10 });
expect(wrapper.find('span').text()).toBe('10');
});
});
测试 Props 传递 #
javascript
function Parent() {
return <Child title="Hello" count={5} />;
}
function Child({ title, count }) {
return (
<div>
<h1>{title}</h1>
<span>{count}</span>
</div>
);
}
describe('Props passing', () => {
it('passes props to child', () => {
const wrapper = shallow(<Parent />);
const child = wrapper.find(Child);
expect(child.prop('title')).toBe('Hello');
expect(child.prop('count')).toBe(5);
});
});
Props 验证示例 #
javascript
function UserCard({ user }) {
if (!user) {
return <div>No user data</div>;
}
return (
<div className="user-card">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
describe('UserCard props validation', () => {
it('renders with user data', () => {
const user = { name: 'John', email: 'john@example.com' };
const wrapper = shallow(<UserCard user={user} />);
expect(wrapper.find('h2').text()).toBe('John');
expect(wrapper.find('p').text()).toBe('john@example.com');
});
it('handles missing user', () => {
const wrapper = shallow(<UserCard user={null} />);
expect(wrapper.text()).toBe('No user data');
});
});
State 测试 #
读取 State #
state() 方法 #
javascript
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
step: 1
};
}
render() {
return (
<div>
<span className="count">{this.state.count}</span>
<span className="step">{this.state.step}</span>
</div>
);
}
}
describe('Counter state', () => {
it('has initial state', () => {
const wrapper = shallow(<Counter />);
// 获取整个 state
expect(wrapper.state()).toEqual({ count: 0, step: 1 });
// 获取单个 state 值
expect(wrapper.state('count')).toBe(0);
expect(wrapper.state('step')).toBe(1);
});
});
设置 State #
setState() 方法 #
javascript
describe('Counter setState', () => {
it('updates state', () => {
const wrapper = shallow(<Counter />);
// 设置单个 state 值
wrapper.setState({ count: 5 });
expect(wrapper.state('count')).toBe(5);
// 设置多个 state 值
wrapper.setState({ count: 10, step: 2 });
expect(wrapper.state()).toEqual({ count: 10, step: 2 });
});
it('re-renders after setState', () => {
const wrapper = shallow(<Counter />);
wrapper.setState({ count: 5 });
expect(wrapper.find('.count').text()).toBe('5');
});
});
测试 State 变化 #
javascript
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = { isOn: false };
}
toggle = () => {
this.setState(prevState => ({
isOn: !prevState.isOn
}));
}
render() {
return (
<div>
<span className="status">
{this.state.isOn ? 'ON' : 'OFF'}
</span>
<button onClick={this.toggle}>Toggle</button>
</div>
);
}
}
describe('Toggle state changes', () => {
it('toggles state on click', () => {
const wrapper = shallow(<Toggle />);
expect(wrapper.state('isOn')).toBe(false);
expect(wrapper.find('.status').text()).toBe('OFF');
wrapper.find('button').simulate('click');
expect(wrapper.state('isOn')).toBe(true);
expect(wrapper.find('.status').text()).toBe('ON');
});
it('can set state directly', () => {
const wrapper = shallow(<Toggle />);
wrapper.setState({ isOn: true });
expect(wrapper.state('isOn')).toBe(true);
expect(wrapper.find('.status').text()).toBe('ON');
});
});
函数组件 State #
useState 测试 #
javascript
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span className="count">{count}</span>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
</div>
);
}
describe('Counter with useState', () => {
it('has initial count of 0', () => {
const wrapper = shallow(<Counter />);
// 函数组件的 state 可以通过查找元素来验证
expect(wrapper.find('.count').text()).toBe('0');
});
it('increments count', () => {
const wrapper = shallow(<Counter />);
wrapper.find('button').at(0).simulate('click');
expect(wrapper.find('.count').text()).toBe('1');
});
it('decrements count', () => {
const wrapper = shallow(<Counter />);
wrapper.find('button').at(1).simulate('click');
expect(wrapper.find('.count').text()).toBe('-1');
});
});
多个 useState #
javascript
function Form() {
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
return (
<form>
<input
name="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</form>
);
}
describe('Form with multiple useState', () => {
it('updates username', () => {
const wrapper = shallow(<Form />);
wrapper.find('input[name="username"]').simulate('change', {
target: { value: 'john' }
});
expect(wrapper.find('input[name="username"]').prop('value')).toBe('john');
});
it('updates email', () => {
const wrapper = shallow(<Form />);
wrapper.find('input[name="email"]').simulate('change', {
target: { value: 'john@example.com' }
});
expect(wrapper.find('input[name="email"]').prop('value')).toBe('john@example.com');
});
});
实用示例 #
测试复杂状态 #
javascript
class ShoppingCart extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [],
total: 0
};
}
addItem = (item) => {
this.setState(prevState => ({
items: [...prevState.items, item],
total: prevState.total + item.price
}));
}
removeItem = (itemId) => {
this.setState(prevState => {
const item = prevState.items.find(i => i.id === itemId);
return {
items: prevState.items.filter(i => i.id !== itemId),
total: prevState.total - (item ? item.price : 0)
};
});
}
render() {
return (
<div>
<ul className="items">
{this.state.items.map(item => (
<li key={item.id}>
{item.name} - ${item.price}
<button onClick={() => this.removeItem(item.id)}>
Remove
</button>
</li>
))}
</ul>
<span className="total">${this.state.total}</span>
</div>
);
}
}
describe('ShoppingCart', () => {
it('starts with empty cart', () => {
const wrapper = shallow(<ShoppingCart />);
expect(wrapper.state('items')).toEqual([]);
expect(wrapper.state('total')).toBe(0);
});
it('adds item correctly', () => {
const wrapper = shallow(<ShoppingCart />);
const item = { id: 1, name: 'Product', price: 100 };
wrapper.instance().addItem(item);
expect(wrapper.state('items')).toEqual([item]);
expect(wrapper.state('total')).toBe(100);
});
it('removes item correctly', () => {
const wrapper = shallow(<ShoppingCart />);
const item1 = { id: 1, name: 'Product 1', price: 100 };
const item2 = { id: 2, name: 'Product 2', price: 200 };
wrapper.instance().addItem(item1);
wrapper.instance().addItem(item2);
wrapper.instance().removeItem(1);
expect(wrapper.state('items')).toEqual([item2]);
expect(wrapper.state('total')).toBe(200);
});
it('can set state directly', () => {
const wrapper = shallow(<ShoppingCart />);
wrapper.setState({
items: [
{ id: 1, name: 'Product', price: 100 }
],
total: 100
});
expect(wrapper.find('.items').children().length).toBe(1);
expect(wrapper.find('.total').text()).toBe('$100');
});
});
测试 Props 变化 #
javascript
function UserProfile({ user, onUpdate }) {
const [isEditing, setIsEditing] = useState(false);
const [name, setName] = useState(user.name);
useEffect(() => {
setName(user.name);
}, [user.name]);
return (
<div>
{isEditing ? (
<input
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={() => {
onUpdate(name);
setIsEditing(false);
}}
/>
) : (
<span onClick={() => setIsEditing(true)}>{name}</span>
)}
</div>
);
}
describe('UserProfile props changes', () => {
it('updates name when user prop changes', () => {
const mockUpdate = jest.fn();
const wrapper = shallow(
<UserProfile user={{ name: 'John' }} onUpdate={mockUpdate} />
);
expect(wrapper.find('span').text()).toBe('John');
wrapper.setProps({ user: { name: 'Jane' } });
expect(wrapper.find('span').text()).toBe('Jane');
});
});
测试条件渲染 #
javascript
function ConditionalDisplay({ isLoggedIn, user }) {
return (
<div>
{isLoggedIn ? (
<div className="welcome">
Welcome, {user.name}!
</div>
) : (
<div className="login-prompt">
Please log in
</div>
)}
</div>
);
}
describe('ConditionalDisplay', () => {
it('shows welcome when logged in', () => {
const wrapper = shallow(
<ConditionalDisplay
isLoggedIn={true}
user={{ name: '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(
<ConditionalDisplay
isLoggedIn={false}
user={null}
/>
);
expect(wrapper.find('.login-prompt').exists()).toBe(true);
expect(wrapper.find('.welcome').exists()).toBe(false);
});
it('updates display when props change', () => {
const wrapper = shallow(
<ConditionalDisplay
isLoggedIn={false}
user={null}
/>
);
expect(wrapper.find('.login-prompt').exists()).toBe(true);
wrapper.setProps({
isLoggedIn: true,
user: { name: 'John' }
});
expect(wrapper.find('.welcome').exists()).toBe(true);
});
});
高级技巧 #
使用 instance() 访问方法 #
javascript
class Calculator extends React.Component {
constructor(props) {
super(props);
this.state = { result: 0 };
}
add = (a, b) => {
const result = a + b;
this.setState({ result });
return result;
}
subtract = (a, b) => {
const result = a - b;
this.setState({ result });
return result;
}
render() {
return <div>{this.state.result}</div>;
}
}
describe('Calculator instance methods', () => {
it('can call instance methods', () => {
const wrapper = shallow(<Calculator />);
const instance = wrapper.instance();
const result = instance.add(5, 3);
expect(result).toBe(8);
expect(wrapper.state('result')).toBe(8);
expect(wrapper.text()).toBe('8');
});
});
测试异步状态更新 #
javascript
class AsyncComponent extends React.Component {
constructor(props) {
super(props);
this.state = { data: null, loading: false };
}
fetchData = async () => {
this.setState({ loading: true });
const data = await this.props.fetchData();
this.setState({ data, loading: false });
}
render() {
const { data, loading } = this.state;
if (loading) return <div>Loading...</div>;
if (!data) return <button onClick={this.fetchData}>Load</button>;
return <div>{data}</div>;
}
}
describe('AsyncComponent', () => {
it('handles async state updates', async () => {
const mockFetch = jest.fn().mockResolvedValue('test data');
const wrapper = shallow(<AsyncComponent fetchData={mockFetch} />);
wrapper.find('button').simulate('click');
expect(wrapper.state('loading')).toBe(true);
expect(wrapper.text()).toBe('Loading...');
// 等待异步操作完成
await new Promise(resolve => setTimeout(resolve, 0));
wrapper.update();
expect(wrapper.state('loading')).toBe(false);
expect(wrapper.state('data')).toBe('test data');
});
});
使用 update() 强制更新 #
javascript
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
describe('Counter with update', () => {
it('updates display after state change', () => {
const wrapper = shallow(<Counter />);
wrapper.find('button').simulate('click');
// 有时需要手动调用 update
wrapper.update();
expect(wrapper.find('span').text()).toBe('1');
});
});
最佳实践 #
1. 优先测试渲染结果 #
javascript
// ✅ 好的做法 - 测试渲染结果
it('displays correct count', () => {
const wrapper = shallow(<Counter />);
wrapper.find('button').simulate('click');
expect(wrapper.find('.count').text()).toBe('1');
});
// ⚠️ 谨慎使用 - 测试内部状态
it('has correct state', () => {
const wrapper = shallow(<Counter />);
wrapper.find('button').simulate('click');
expect(wrapper.state('count')).toBe(1); // 实现细节
});
2. 使用 setState/setProps 进行边界测试 #
javascript
describe('Edge cases', () => {
it('handles empty array', () => {
const wrapper = shallow(<List items={[]} />);
wrapper.setProps({ items: [] });
expect(wrapper.find('.empty-message').exists()).toBe(true);
});
it('handles large numbers', () => {
const wrapper = shallow(<Counter />);
wrapper.setState({ count: Number.MAX_SAFE_INTEGER });
expect(wrapper.find('.count').text()).toBe(String(Number.MAX_SAFE_INTEGER));
});
});
3. 测试状态转换 #
javascript
describe('State transitions', () => {
it('transitions through states correctly', () => {
const wrapper = shallow(<Form />);
// 初始状态
expect(wrapper.state('step')).toBe(1);
// 下一步
wrapper.find('.next-button').simulate('click');
expect(wrapper.state('step')).toBe(2);
// 上一步
wrapper.find('.prev-button').simulate('click');
expect(wrapper.state('step')).toBe(1);
});
});
下一步 #
现在你已经掌握了状态与属性测试的方法,接下来学习 生命周期测试 了解如何测试组件的生命周期方法!
最后更新:2026-03-28