Skip to content

Redux - 测试

测试 Redux 应用通常非常简单,因为 Redux 主要依赖于纯函数(pure functions),特别是 reducer。Action 通常是简单的对象,而 action creator 往往是返回这些对象的简单函数。这使得单元测试(unit testing)无需大量模拟(mocking)即可轻松进行。

我们将使用 Jest 作为我们的测试框架。Jest 是一个流行的 JavaScript 测试框架,它在 Node.js 环境中运行良好,并且对于大多数 Redux 测试来说不需要 DOM。

如果您还没有安装 Jest,请运行:

npm install --save-dev jest
# or
yarn add --dev jest
# or
pnpm add --save-dev jest

如果您使用 Babel 或 TypeScript,请确保 Jest 配置正确。对于 Babel,您可能需要安装 babel-jest:

npm install --save-dev babel-jest @babel/core @babel/preset-env
# or
yarn add --dev babel-jest @babel/core @babel/preset-env
# or
pnpm add --save-dev babel-jest @babel/core @babel/preset-env

通过 babel.config.js 或 .babelrc 配置 Babel。一个简单的配置可能如下所示:

// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
// Add other presets like @babel/preset-react or @babel/preset-typescript if needed
],
};

将测试脚本添加到您的 package.json 文件中:

{
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}
}

使用 npm test、yarn test 或 pnpm test 运行测试。

Action creator 是返回 action 对象的函数。测试它们涉及调用该函数并断言返回的对象与期望的 action 匹配。

考虑这个简单的 action creator:

// actions/itemActions.js
export const ITEMS_REQUEST_SUCCESS = 'items/requestSuccess';
export const itemsRequestSuccess = (isLoading) => ({
type: ITEMS_REQUEST_SUCCESS,
payload: { isLoading },
});

这个 action creator 的测试用例如下:

// actions/itemActions.test.js
import { itemsRequestSuccess, ITEMS_REQUEST_SUCCESS } from './itemActions';
describe('item actions', () => {
it('itemsRequestSuccess should create an action with isLoading payload', () => {
const isLoading = false;
const expectedAction = {
type: ITEMS_REQUEST_SUCCESS,
payload: { isLoading },
};
expect(itemsRequestSuccess(isLoading)).toEqual(expectedAction);
});
});

如果您使用 Redux Toolkit 的 createSlice,action creator 会自动生成。您通常通过 reducer 测试或集成测试(integration tests)来测试它们的效果。

Reducer 必须是纯函数(pure functions):给定相同的 state 和 action,它们必须总是返回相同的新 state,且没有副作用(side effects)或突变(mutations)。

考虑一个简单的 reducer(可能是 Redux Toolkit 生成的 slice 的一部分):

// reducers/itemReducer.js
const initialState = {
isLoading: false,
items: [],
};
const itemReducer = (state = initialState, action) => {
switch (action.type) {
case 'items/requestStart':
return {
...state,
isLoading: true,
};
case 'items/requestSuccess':
return {
...state,
isLoading: action.payload.isLoading, // 假设 payload: { isLoading: boolean }
};
// 带有 items payload 的例子
case 'items/set':
return {
...state,
items: action.payload, // 假设 payload 是一个 items 数组
};
default:
return state;
}
};
export default itemReducer;

要测试这个 reducer,提供一个初始 state,分发(dispatch)一个 action,并断言返回的 state 是正确的:

// reducers/itemReducer.test.js
import itemReducer from './itemReducer';
describe('itemReducer', () => {
const initialState = {
isLoading: false,
items: [],
};
it('should return the initial state if no state is provided', () => {
expect(itemReducer(undefined, {})).toEqual(initialState);
});
it('should handle items/requestStart action', () => {
const action = { type: 'items/requestStart' };
const expectedState = {
...initialState,
isLoading: true,
};
expect(itemReducer(initialState, action)).toEqual(expectedState);
});
it('should handle items/requestSuccess action', () => {
const previousState = { ...initialState, isLoading: true };
const action = {
type: 'items/requestSuccess',
payload: { isLoading: false },
};
const expectedState = {
...initialState,
isLoading: false,
};
expect(itemReducer(previousState, action)).toEqual(expectedState);
});
it('should handle items/set action', () => {
const action = {
type: 'items/set',
payload: [{ id: 1, name: 'Test Item' }],
};
const expectedState = {
...initialState,
items: [{ id: 1, name: 'Test Item' }],
};
expect(itemReducer(initialState, action)).toEqual(expectedState);
});
});

测试异步 action creator(例如使用 createAsyncThunk 创建的 thunk 或手动创建的 thunk)通常涉及模拟 API 调用并检查哪些 action 被分发了。

可以使用像 redux-mock-store 这样的库,或者通过模拟 dispatch 和 getState 直接测试 thunk。

使用 Jest 模拟的例子(概念性):

// actions/asyncActions.test.js
import { createAsyncThunk } from '@reduxjs/toolkit';
import * as api from '../api'; // 你的 API 调用模块
// Assume fetchItems is an async thunk
// const fetchItems = createAsyncThunk('items/fetch', async () => { ... });
jest.mock('../api'); // 模拟 API 模块
describe('async actions', () => {
it('fetchItems dispatches pending and fulfilled actions on success', async () => {
const mockDispatch = jest.fn();
const mockGetState = jest.fn();
const mockData = [{ id: 1, name: 'Fetched Item' }];
api.fetchItemsFromApi.mockResolvedValue(mockData); // 模拟 API 调用
// Assume fetchItems is your async thunk function
// await fetchItems()(mockDispatch, mockGetState, undefined);
// Assertions would depend on how fetchItems is implemented (e.g., using createAsyncThunk)
// 断言将取决于 fetchItems 如何实现(例如,使用 createAsyncThunk)
// Example: Check if pending action was dispatched
// 示例:检查 pending action 是否被分发
// expect(mockDispatch).toHaveBeenCalledWith(expect.objectContaining({ type: 'items/fetch/pending' }));
// Example: Check if fulfilled action was dispatched with correct payload
// 示例:检查 fulfilled action 是否带有正确的 payload 被分发
// expect(mockDispatch).toHaveBeenCalledWith(expect.objectContaining({
// type: 'items/fetch/fulfilled',
// payload: mockData,
// }));
});
});

对于更复杂的场景或集成测试,考虑使用 Testing Library 等库配合真实的 Redux store 进行测试。

有关更多详细信息,请参阅官方文档: