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