Enzyme Context 测试 #

Context 概述 #

React Context 提供了一种在组件树中共享数据的方式,无需手动传递 props。测试 Context 时需要特别注意 Provider 和 Consumer 的配合。

Context 结构 #

text
┌─────────────────────────────────────────────────────────────┐
│                    React Context 结构                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              Context.Provider                        │   │
│  │  value={{ data, updateData }}                        │   │
│  │  ┌─────────────────────────────────────────────┐   │   │
│  │  │              Child Component                 │   │   │
│  │  │  • useContext(Context)                       │   │   │
│  │  │  • <Context.Consumer>                        │   │   │
│  │  │  • contextType (class component)             │   │   │
│  │  └─────────────────────────────────────────────┘   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

基本 Context 测试 #

创建 Context #

javascript
const ThemeContext = React.createContext('light');

function ThemedButton() {
  const theme = useContext(ThemeContext);
  
  return (
    <button className={`btn btn-${theme}`}>
      Current theme: {theme}
    </button>
  );
}

测试默认值 #

javascript
describe('ThemedButton default value', () => {
  it('uses default context value', () => {
    const wrapper = shallow(<ThemedButton />);
    
    expect(wrapper.hasClass('btn-light')).toBe(true);
    expect(wrapper.text()).toBe('Current theme: light');
  });
});

测试 Provider 提供的值 #

javascript
describe('ThemedButton with Provider', () => {
  it('uses provided context value', () => {
    const wrapper = mount(
      <ThemeContext.Provider value="dark">
        <ThemedButton />
      </ThemeContext.Provider>
    );
    
    expect(wrapper.find('button').hasClass('btn-dark')).toBe(true);
    expect(wrapper.find('button').text()).toBe('Current theme: dark');
    
    wrapper.unmount();
  });
  
  it('updates when context value changes', () => {
    const wrapper = mount(
      <ThemeContext.Provider value="light">
        <ThemedButton />
      </ThemeContext.Provider>
    );
    
    expect(wrapper.find('button').hasClass('btn-light')).toBe(true);
    
    wrapper.setProps({ value: 'dark' });
    
    expect(wrapper.find('button').hasClass('btn-dark')).toBe(true);
    
    wrapper.unmount();
  });
});

复杂 Context 测试 #

用户认证 Context #

javascript
const AuthContext = React.createContext({
  user: null,
  login: () => {},
  logout: () => {}
});

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  
  const login = useCallback((userData) => {
    setUser(userData);
  }, []);
  
  const logout = useCallback(() => {
    setUser(null);
  }, []);
  
  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

function UserProfile() {
  const { user, login, logout } = useContext(AuthContext);
  
  if (!user) {
    return (
      <div>
        <p>Please log in</p>
        <button onClick={() => login({ name: 'John' })}>
          Login
        </button>
      </div>
    );
  }
  
  return (
    <div>
      <p>Welcome, {user.name}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

describe('AuthContext', () => {
  describe('UserProfile', () => {
    it('shows login prompt when not authenticated', () => {
      const wrapper = mount(
        <AuthContext.Provider value={{ 
          user: null, 
          login: jest.fn(), 
          logout: jest.fn() 
        }}>
          <UserProfile />
        </AuthContext.Provider>
      );
      
      expect(wrapper.find('p').text()).toBe('Please log in');
      expect(wrapper.find('button').text()).toBe('Login');
      
      wrapper.unmount();
    });
    
    it('shows user info when authenticated', () => {
      const wrapper = mount(
        <AuthContext.Provider value={{ 
          user: { name: 'John' }, 
          login: jest.fn(), 
          logout: jest.fn() 
        }}>
          <UserProfile />
        </AuthContext.Provider>
      );
      
      expect(wrapper.find('p').text()).toBe('Welcome, John');
      expect(wrapper.find('button').text()).toBe('Logout');
      
      wrapper.unmount();
    });
    
    it('calls login on button click', () => {
      const mockLogin = jest.fn();
      
      const wrapper = mount(
        <AuthContext.Provider value={{ 
          user: null, 
          login: mockLogin, 
          logout: jest.fn() 
        }}>
          <UserProfile />
        </AuthContext.Provider>
      );
      
      wrapper.find('button').simulate('click');
      
      expect(mockLogin).toHaveBeenCalledWith({ name: 'John' });
      
      wrapper.unmount();
    });
    
    it('calls logout on button click', () => {
      const mockLogout = jest.fn();
      
      const wrapper = mount(
        <AuthContext.Provider value={{ 
          user: { name: 'John' }, 
          login: jest.fn(), 
          logout: mockLogout 
        }}>
          <UserProfile />
        </AuthContext.Provider>
      );
      
      wrapper.find('button').simulate('click');
      
      expect(mockLogout).toHaveBeenCalled();
      
      wrapper.unmount();
    });
  });
  
  describe('AuthProvider', () => {
    it('provides login functionality', () => {
      const wrapper = mount(
        <AuthProvider>
          <UserProfile />
        </AuthProvider>
      );
      
      expect(wrapper.find('p').text()).toBe('Please log in');
      
      wrapper.find('button').simulate('click');
      wrapper.update();
      
      expect(wrapper.find('p').text()).toBe('Welcome, John');
      
      wrapper.unmount();
    });
    
    it('provides logout functionality', () => {
      const wrapper = mount(
        <AuthProvider>
          <UserProfile />
        </AuthProvider>
      );
      
      wrapper.find('button').simulate('click');
      wrapper.update();
      
      expect(wrapper.find('p').text()).toBe('Welcome, John');
      
      wrapper.find('button').simulate('click');
      wrapper.update();
      
      expect(wrapper.find('p').text()).toBe('Please log in');
      
      wrapper.unmount();
    });
  });
});

Consumer 组件测试 #

使用 Context.Consumer #

javascript
const LanguageContext = React.createContext('en');

function LanguageSelector() {
  return (
    <LanguageContext.Consumer>
      {(language) => (
        <select value={language}>
          <option value="en">English</option>
          <option value="zh">中文</option>
          <option value="ja">日本語</option>
        </select>
      )}
    </LanguageContext.Consumer>
  );
}

describe('LanguageSelector with Consumer', () => {
  it('displays current language', () => {
    const wrapper = mount(
      <LanguageContext.Provider value="zh">
        <LanguageSelector />
      </LanguageContext.Provider>
    );
    
    expect(wrapper.find('select').prop('value')).toBe('zh');
    
    wrapper.unmount();
  });
  
  it('updates when language changes', () => {
    const wrapper = mount(
      <LanguageContext.Provider value="en">
        <LanguageSelector />
      </LanguageContext.Provider>
    );
    
    expect(wrapper.find('select').prop('value')).toBe('en');
    
    wrapper.setProps({ value: 'ja' });
    
    expect(wrapper.find('select').prop('value')).toBe('ja');
    
    wrapper.unmount();
  });
});

多个 Consumer #

javascript
const ThemeContext = React.createContext('light');
const UserContext = React.createContext({ name: 'Guest' });

function Header() {
  return (
    <ThemeContext.Consumer>
      {(theme) => (
        <UserContext.Consumer>
          {(user) => (
            <header className={`header header-${theme}`}>
              <h1>Welcome, {user.name}</h1>
            </header>
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

describe('Header with multiple Consumers', () => {
  it('uses both contexts', () => {
    const wrapper = mount(
      <ThemeContext.Provider value="dark">
        <UserContext.Provider value={{ name: 'John' }}>
          <Header />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
    
    expect(wrapper.find('header').hasClass('header-dark')).toBe(true);
    expect(wrapper.find('h1').text()).toBe('Welcome, John');
    
    wrapper.unmount();
  });
  
  it('updates when either context changes', () => {
    const wrapper = mount(
      <ThemeContext.Provider value="light">
        <UserContext.Provider value={{ name: 'Guest' }}>
          <Header />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
    
    expect(wrapper.find('header').hasClass('header-light')).toBe(true);
    expect(wrapper.find('h1').text()).toBe('Welcome, Guest');
    
    wrapper.setProps({ value: 'dark' });
    
    expect(wrapper.find('header').hasClass('header-dark')).toBe(true);
    
    wrapper.unmount();
  });
});

Class 组件 Context 测试 #

使用 contextType #

javascript
const SizeContext = React.createContext('medium');

class SizeDisplay extends React.Component {
  static contextType = SizeContext;
  
  render() {
    return (
      <div className={`size-${this.context}`}>
        Current size: {this.context}
      </div>
    );
  }
}

describe('SizeDisplay with contextType', () => {
  it('accesses context via this.context', () => {
    const wrapper = mount(
      <SizeContext.Provider value="large">
        <SizeDisplay />
      </SizeContext.Provider>
    );
    
    expect(wrapper.find('div').hasClass('size-large')).toBe(true);
    expect(wrapper.find('div').text()).toBe('Current size: large');
    
    wrapper.unmount();
  });
  
  it('updates when context changes', () => {
    const wrapper = mount(
      <SizeContext.Provider value="small">
        <SizeDisplay />
      </SizeContext.Provider>
    );
    
    expect(wrapper.find('div').hasClass('size-small')).toBe(true);
    
    wrapper.setProps({ value: 'medium' });
    
    expect(wrapper.find('div').hasClass('size-medium')).toBe(true);
    
    wrapper.unmount();
  });
});

使用 context 选项 #

javascript
describe('SizeDisplay with context option', () => {
  it('can provide context via options', () => {
    const wrapper = shallow(<SizeDisplay />, {
      context: 'extra-large'
    });
    
    expect(wrapper.hasClass('size-extra-large')).toBe(true);
  });
});

测试 Context 更新 #

动态 Context 更新 #

javascript
function ThemeToggler() {
  const [theme, setTheme] = useState('light');
  
  const toggle = () => {
    setTheme(t => t === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      <ThemedButton />
    </ThemeContext.Provider>
  );
}

function ThemedButton() {
  const { theme, toggle } = useContext(ThemeContext);
  
  return (
    <button 
      className={`btn btn-${theme}`}
      onClick={toggle}
    >
      Toggle Theme
    </button>
  );
}

describe('ThemeToggler', () => {
  it('toggles theme on click', () => {
    const wrapper = mount(<ThemeToggler />);
    
    expect(wrapper.find('button').hasClass('btn-light')).toBe(true);
    
    wrapper.find('button').simulate('click');
    wrapper.update();
    
    expect(wrapper.find('button').hasClass('btn-dark')).toBe(true);
    
    wrapper.find('button').simulate('click');
    wrapper.update();
    
    expect(wrapper.find('button').hasClass('btn-light')).toBe(true);
    
    wrapper.unmount();
  });
});

异步 Context 更新 #

javascript
function DataProvider({ children }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  
  const fetchData = async () => {
    setLoading(true);
    const result = await fetch('/api/data').then(r => r.json());
    setData(result);
    setLoading(false);
  };
  
  return (
    <DataContext.Provider value={{ data, loading, fetchData }}>
      {children}
    </DataContext.Provider>
  );
}

const DataContext = React.createContext();

function DataDisplay() {
  const { data, loading, fetchData } = useContext(DataContext);
  
  useEffect(() => {
    fetchData();
  }, []);
  
  if (loading) return <div>Loading...</div>;
  if (!data) return <button onClick={fetchData}>Load</button>;
  return <div>{data.name}</div>;
}

describe('DataProvider async updates', () => {
  beforeEach(() => {
    global.fetch = jest.fn(() =>
      Promise.resolve({
        json: () => Promise.resolve({ name: 'Test Data' })
      })
    );
  });
  
  afterEach(() => {
    global.fetch.mockRestore();
  });
  
  it('fetches and displays data', async () => {
    const wrapper = mount(
      <DataProvider>
        <DataDisplay />
      </DataProvider>
    );
    
    expect(wrapper.text()).toBe('Loading...');
    
    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 0));
    });
    wrapper.update();
    
    expect(wrapper.text()).toBe('Test Data');
    
    wrapper.unmount();
  });
});

多层嵌套 Context #

嵌套 Provider 测试 #

javascript
const ThemeContext = React.createContext('light');
const UserContext = React.createContext(null);
const LanguageContext = React.createContext('en');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <UserContext.Provider value={{ name: 'John' }}>
        <LanguageContext.Provider value="zh">
          <Dashboard />
        </LanguageContext.Provider>
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

function Dashboard() {
  const theme = useContext(ThemeContext);
  const user = useContext(UserContext);
  const language = useContext(LanguageContext);
  
  return (
    <div className={`dashboard dashboard-${theme}`}>
      <h1>{user.name}'s Dashboard</h1>
      <p>Language: {language}</p>
    </div>
  );
}

describe('Nested Context', () => {
  it('receives values from all providers', () => {
    const wrapper = mount(<App />);
    
    expect(wrapper.find('.dashboard').hasClass('dashboard-dark')).toBe(true);
    expect(wrapper.find('h1').text()).toBe("John's Dashboard");
    expect(wrapper.find('p').text()).toBe('Language: zh');
    
    wrapper.unmount();
  });
});

测试部分 Context #

javascript
describe('Dashboard with partial context', () => {
  it('can test with mock context', () => {
    const wrapper = mount(
      <ThemeContext.Provider value="light">
        <UserContext.Provider value={{ name: 'Jane' }}>
          <LanguageContext.Provider value="en">
            <Dashboard />
          </LanguageContext.Provider>
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
    
    expect(wrapper.find('.dashboard').hasClass('dashboard-light')).toBe(true);
    expect(wrapper.find('h1').text()).toBe("Jane's Dashboard");
    expect(wrapper.find('p').text()).toBe('Language: en');
    
    wrapper.unmount();
  });
});

测试辅助函数 #

创建 Context Wrapper #

javascript
function createWrapper(contexts) {
  return function Wrapper({ children }) {
    return contexts.reduceRight(
      (acc, [Context, value]) => (
        <Context.Provider value={value}>{acc}</Context.Provider>
      ),
      children
    );
  };
}

describe('Dashboard with helper', () => {
  it('uses createWrapper helper', () => {
    const Wrapper = createWrapper([
      [ThemeContext, 'dark'],
      [UserContext, { name: 'Test User' }],
      [LanguageContext, 'ja']
    ]);
    
    const wrapper = mount(
      <Wrapper>
        <Dashboard />
      </Wrapper>
    );
    
    expect(wrapper.find('h1').text()).toBe("Test User's Dashboard");
    
    wrapper.unmount();
  });
});

Context Mock 工具 #

javascript
function mockContext(Context, value) {
  return function MockProvider({ children }) {
    return (
      <Context.Provider value={value}>
        {children}
      </Context.Provider>
    );
  };
}

describe('With mockContext', () => {
  it('uses mockContext helper', () => {
    const MockTheme = mockContext(ThemeContext, 'dark');
    
    const wrapper = mount(
      <MockTheme>
        <ThemedButton />
      </MockTheme>
    );
    
    expect(wrapper.find('button').hasClass('btn-dark')).toBe(true);
    
    wrapper.unmount();
  });
});

最佳实践 #

1. 使用 mount 测试 Context #

javascript
// ✅ 好的做法 - 使用 mount
it('tests context with mount', () => {
  const wrapper = mount(
    <Provider>
      <Component />
    </Provider>
  );
  // 断言
  wrapper.unmount();
});

// ❌ 避免 - shallow 不支持 Context 传播
it('tests context with shallow', () => {
  const wrapper = shallow(<Component />); // Context 不会传播
});

2. 测试隔离 #

javascript
describe('Context tests', () => {
  let wrapper;
  
  afterEach(() => {
    if (wrapper) {
      wrapper.unmount();
      wrapper = null;
    }
  });
  
  it('test case 1', () => {
    wrapper = mount(<Provider><Component /></Provider>);
    // 测试代码
  });
});

3. 测试 Context 变化 #

javascript
it('reacts to context changes', () => {
  const wrapper = mount(
    <Context.Provider value="initial">
      <Component />
    </Context.Provider>
  );
  
  // 验证初始值
  expect(wrapper.find('.value').text()).toBe('initial');
  
  // 更新 Context
  wrapper.setProps({ value: 'updated' });
  
  // 验证更新后的值
  expect(wrapper.find('.value').text()).toBe('updated');
  
  wrapper.unmount();
});

4. 测试默认值 #

javascript
it('uses default context value', () => {
  // 不使用 Provider,测试默认值
  const wrapper = mount(<Component />);
  
  expect(wrapper.find('.value').text()).toBe('default value');
  
  wrapper.unmount();
});

下一步 #

现在你已经掌握了 Context 测试的方法,接下来学习 最佳实践 了解 Enzyme 测试的最佳实践和常见模式!

最后更新:2026-03-28