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