Skip to main content

编写测试

¥Writing Tests

你将学到什么
  • 使用 Redux 测试应用的推荐实践

    ¥Recommended practices for testing apps using Redux

  • 测试配置和设置示例

    ¥Examples of test configuration and setup

指导原则

¥Guiding Principles

测试 Redux 逻辑的指导原则紧密遵循 React 测试库的指导原则:

¥The guiding principles for testing Redux logic closely follow that of React Testing Library:

你的测试越接近软件的使用方式,它们就越能给你带来信心。 - 肯特 C.多兹

¥The more your tests resemble the way your software is used, the more confidence they can give you. - Kent C. Dodds

因为你编写的大多数 Redux 代码都是函数,而且其中许多是纯函数,因此无需模拟即可轻松测试。但是,你应该考虑 Redux 代码的每一段是否都需要自己的专用测试。在大多数场景中,终端用户不知道,也不关心应用中是否使用了 Redux。因此,Redux 代码可以被视为应用的实现细节,在许多情况下不需要对 Redux 代码进行显式测试。

¥Because most of the Redux code you write are functions, and many of them are pure, they are easy to test without mocking. However, you should consider whether each piece of your Redux code needs its own dedicated tests. In the majority of scenarios, the end-user does not know, and does not care whether Redux is used within the application at all. As such, the Redux code can be treated as an implementation detail of the app, without requiring explicit tests for the Redux code in many circumstances.

我们对于使用 Redux 测试应用的一般建议是:

¥Our general advice for testing an app using Redux is:

  • 更喜欢编写所有东西一起工作的集成测试。对于使用 Redux 的 React 应用,使用封装正在测试的组件的真实存储实例渲染 <Provider>。与正在测试的页面的交互应该使用真正的 Redux 逻辑,并模拟 API 调用,这样应用代码就不必更改,并断言 UI 已适当更新。

    ¥Prefer writing integration tests with everything working together. For a React app using Redux, render a <Provider> with a real store instance wrapping the components being tested. Interactions with the page being tested should use real Redux logic, with API calls mocked out so app code doesn't have to change, and assert that the UI is updated appropriately.

  • 如果需要,请对纯函数(例如特别复杂的 reducer 或选择器)使用基本单元测试。然而,在许多情况下,这些只是集成测试所涵盖的实现细节。

    ¥If needed, use basic unit tests for pure functions such as particularly complex reducers or selectors. However, in many cases, these are just implementation details that are covered by integration tests instead.

  • 不要尝试模拟选择器函数或 React-Redux 钩子!从库中模拟导入很脆弱,并且无法让你确信实际的应用代码正在运行。

    ¥Do not try to mock selector functions or the React-Redux hooks! Mocking imports from libraries is fragile, and doesn't give you confidence that your actual app code is working.

信息

有关我们推荐集成式测试的背景,请参阅:

¥For background on why we recommend integration-style tests, see:

设置测试环境

¥Setting Up a Test Environment

测试运行器

¥Test Runners

Redux 可以使用任何测试运行器进行测试,因为它只是纯 JavaScript。一个常见的选项是 Jest,这是一种广泛使用的测试运行器,随 Create-React-App 一起提供,并由 Redux 库存储库使用。如果你使用 Vite 来构建项目,则可能会使用 Vitest 作为测试运行程序。

¥Redux can be tested with any test runner, since it's just plain JavaScript. One common option is Jest, a widely used test runner that comes with Create-React-App, and is used by the Redux library repos. If you're using Vite to build your project, you may be using Vitest as your test runner.

通常,你的测试运行器需要配置为编译 JavaScript/TypeScript 语法。如果你要测试 UI 组件,你可能需要配置测试运行器以使用 JSDOM 来提供模拟 DOM 环境。

¥Typically, your test runner needs to be configured to compile JavaScript/TypeScript syntax. If you're going to be testing UI components, you will likely need to configure the test runner to use JSDOM to provide a mock DOM environment.

本页中的示例假设你使用的是 Jest,但无论你使用什么测试运行程序,都适用相同的模式。

¥The examples in this page will assume you're using Jest, but the same patterns apply no matter what test runner you're using.

请参阅以下资源以获取典型的测试运行器配置说明:

¥See these resources for typical test runner configuration instructions:

UI 和网络测试工具

¥UI and Network Testing Tools

Redux 团队建议使用 React 测试库 (RTL) 来测试连接到 Redux 的 React 组件。React 测试库是一个简单而完整的 React DOM 测试实用程序,鼓励良好的测试实践。它使用 ReactDOM 的 render 函数和 react-dom/tests-utils 中的 act。(测试库工具系列还包括 以及许多其他流行框架的适配器。)

¥The Redux team recommends using React Testing Library (RTL) to test React components that connect to Redux. React Testing Library is a simple and complete React DOM testing utility that encourages good testing practices. It uses ReactDOM's render function and act from react-dom/tests-utils. (The Testing Library family of tools also includes adapters for many other popular frameworks as well.)

我们还建议使用 模拟服务工作进程 (MSW) 来模拟网络请求,因为这意味着在编写测试时不需要更改或模拟你的应用逻辑。

¥We also recommend using Mock Service Worker (MSW) to mock network requests, as this means your application logic does not need to be changed or mocked when writing tests.

集成测试连接的组件和 Redux 逻辑

¥Integration Testing Connected Components and Redux Logic

我们建议通过集成测试来测试 Redux 连接的 React 组件,其中包括所有协同工作的内容,并使用断言来验证当用户以给定方式与其交互时应用的行为是否符合你的预期。

¥Our recommendation for testing Redux-connected React components is via integration tests that include everything working together, with assertions aimed at verifying that the app behaves the way you expect when the user interacts with it in a given manner.

示例应用代码

¥Example App Code

考虑以下 userSlice 切片、存储和 App 组件:

¥Consider the following userSlice slice, store, and App component:

features/users/usersSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
import type { RootState } from '../../app/store'

export const fetchUser = createAsyncThunk('user/fetchUser', async () => {
const response = await userAPI.fetchUser()
return response.data
})

interface UserState {
name: string
status: 'idle' | 'loading' | 'complete'
}

const initialState: UserState = {
name: 'No user',
status: 'idle'
}

const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: builder => {
builder.addCase(fetchUser.pending, (state, action) => {
state.status = 'loading'
})
builder.addCase(fetchUser.fulfilled, (state, action) => {
state.status = 'complete'
state.name = action.payload
})
}
})

export const selectUserName = (state: RootState) => state.user.name
export const selectUserFetchStatus = (state: RootState) => state.user.status

export default userSlice.reducer
app/store.ts
import { combineReducers, configureStore } from '@reduxjs/toolkit'
import userReducer from '../features/users/userSlice'
// Create the root reducer independently to obtain the RootState type
const rootReducer = combineReducers({
user: userReducer
})
export function setupStore(preloadedState?: Partial<RootState>) {
return configureStore({
reducer: rootReducer,
preloadedState
})
}
export type RootState = ReturnType<typeof rootReducer>
export type AppStore = ReturnType<typeof setupStore>
export type AppDispatch = AppStore['dispatch']
app/hooks.ts
import { useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, RootState } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
features/users/UserDisplay.tsx
import React from 'react'
import { useAppDispatch, useAppSelector } from '../../app/hooks'
import { fetchUser, selectUserName, selectUserFetchStatus } from './userSlice'

export default function UserDisplay() {
const dispatch = useAppDispatch()
const userName = useAppSelector(selectUserName)
const userFetchStatus = useAppSelector(selectUserFetchStatus)

return (
<div>
{/* Display the current user name */}
<div>{userName}</div>
{/* On button click, dispatch a thunk action to fetch a user */}
<button onClick={() => dispatch(fetchUser())}>Fetch user</button>
{/* At any point if we're fetching a user, display that on the UI */}
{userFetchStatus === 'loading' && <div>Fetching user...</div>}
</div>
)
}

这个应用涉及 thunk、reducer 和选择器。所有这些都可以通过编写集成测试来测试,并记住以下几点:

¥This app involves thunks, reducers and selectors. All of these can be tested by writing an integration test with the following in mind:

  • 首次加载应用时,应该还没有用户 - 我们应该在屏幕上看到 '没有用户'。

    ¥Upon first loading the app, there should be no user yet - we should see 'No user' on the screen.

  • 单击显示 '获取用户' 的按钮后,我们希望它开始获取用户。我们应该看到 '正在获取用户...' 显示在屏幕上。

    ¥After clicking the button that says 'Fetch user', we expect it to start fetching the user. We should see 'Fetching user...' displayed on the screen.

  • 一段时间后,应该会收到用户。我们不应该再看到 '正在获取用户...',而应该根据 API 的响应看到预期的用户名。

    ¥After some time, the user should be received. We should no longer see 'Fetching user...', but instead should see the expected user's name based on the response from our API.

编写我们的测试以关注上述整体,我们可以避免模拟尽可能多的应用。我们还将相信,当我们以我们期望用户使用应用的方式进行交互时,我们的应用的关键行为会达到我们的期望。

¥Writing our tests to focus on the above as a whole, we can avoid mocking as much of the app as possible. We will also have confidence that the critical behavior of our app does what we expect it to when interacted with in the way we expect the user to use the app.

为了测试该组件,我们将其 render 到 DOM 中,并断言应用以我们期望用户使用应用的方式响应交互。

¥To test the component, we render it into the DOM, and assert that the app responds to interactions in the way we expect the user to use the app.

设置可重用的测试渲染函数

¥Setting Up a Reusable Test Render Function

React 测试库的 render 函数接受 React 元素树并渲染这些组件。就像在真实的应用中一样,任何 Redux 连接的组件都需要 封装着它们的 React-Redux <Provider> 组件,并设置并提供真实的 Redux 存储。

¥React Testing Library's render function accepts a tree of React elements and renders those components. Just like in a real app, any Redux-connected components will need a React-Redux <Provider> component wrapped around them, with a real Redux store set up and provided.

此外,测试代码应该为每个测试创建一个单独的 Redux 存储实例,而不是重用相同的存储实例并重置其状态。这确保了测试之间不会意外泄漏任何值。

¥Additionally, the test code should create a separate Redux store instance for every test, rather than reusing the same store instance and resetting its state. That ensures no values accidentally leak between tests.

我们可以在 render 函数中使用 wrapper 选项,并导出我们自己的自定义 renderWithProviders 函数,该函数创建一个新的 Redux 存储并渲染 <Provider>,而不是在每个测试中复制粘贴相同的存储创建和 Provider 设置,如 React 测试库的设置文档 中所述。

¥Instead of copy-pasting the same store creation and Provider setup in every test, we can use the wrapper option in the render function and export our own customized renderWithProviders function that creates a new Redux store and renders a <Provider>, as explained in React Testing Library's setup docs.

自定义渲染函数应该让我们:

¥The custom render function should let us:

  • 每次调用时创建一个新的 Redux store 实例,并带有一个可选的 preloadedState 值,可用于初始值

    ¥Create a new Redux store instance every time it's called, with an optional preloadedState value that can be used for an initial value

  • 或者传入一个已经创建的 Redux 存储实例

    ¥Alternately pass in an already-created Redux store instance

  • 将附加选项传递给 RTL 的原始 render 函数

    ¥Pass through additional options to RTL's original render function

  • 自动用 <Provider store={store}> 封装正在测试的组件

    ¥Automatically wrap the component being tested with a <Provider store={store}>

  • 返回存储实例,以防测试需要调度更多操作或检查状态

    ¥Return the store instance in case the test needs to dispatch more actions or check state

典型的自定义渲染函数设置可能如下所示:

¥A typical custom render function setup could look like this:

utils/test-utils.tsx
import React, { PropsWithChildren } from 'react'
import { render } from '@testing-library/react'
import type { RenderOptions } from '@testing-library/react'
import { configureStore } from '@reduxjs/toolkit'
import { Provider } from 'react-redux'

import type { AppStore, RootState } from '../app/store'
import { setupStore } from '../app/store'
// As a basic setup, import your same slice reducers
import userReducer from '../features/users/userSlice'

// This type interface extends the default options for render from RTL, as well
// as allows the user to specify other things such as initialState, store.
interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
preloadedState?: Partial<RootState>
store?: AppStore
}

export function renderWithProviders(
ui: React.ReactElement,
extendedRenderOptions: ExtendedRenderOptions = {}
) {
const {
preloadedState = {},
// Automatically create a store instance if no store was passed in
store = setupStore(preloadedState),
...renderOptions
} = extendedRenderOptions

const Wrapper = ({ children }: PropsWithChildren) => (
<Provider store={store}>{children}</Provider>
)

// Return an object with the store and all of RTL's query functions
return {
store,
...render(ui, { wrapper: Wrapper, ...renderOptions })
}
}

在此示例中,我们直接导入实际应用用于创建存储的相同切片 reducer。创建一个可重用的 setupStore 函数可能会有所帮助,该函数使用正确的选项和配置来创建实际的存储,然后在自定义渲染函数中使用它。

¥In this example, we're directly importing the same slice reducers that the real app uses to create the store. It may be helpful to create a reusable setupStore function that does the actual store creation with the right options and configuration, and use that in the custom render function instead.

app/store.ts
import { combineReducers, configureStore } from '@reduxjs/toolkit'

import userReducer from '../features/users/userSlice'

// Create the root reducer separately so we can extract the RootState type
const rootReducer = combineReducers({
user: userReducer
})

export const setupStore = (preloadedState?: Partial<RootState>) => {
return configureStore({
reducer: rootReducer,
preloadedState
})
}

export type RootState = ReturnType<typeof rootReducer>
export type AppStore = ReturnType<typeof setupStore>
export type AppDispatch = AppStore['dispatch']

然后,在测试 utils 文件中使用 setupStore,而不是再次调用 configureStore

¥Then, use setupStore in the test utils file instead of calling configureStore again:

import React, { PropsWithChildren } from 'react'
import { render } from '@testing-library/react'
import type { RenderOptions } from '@testing-library/react'
import { Provider } from 'react-redux'

import { setupStore } from '../app/store'
import type { AppStore, RootState } from '../app/store'

// This type interface extends the default options for render from RTL, as well
// as allows the user to specify other things such as initialState, store.
interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
preloadedState?: Partial<RootState>
store?: AppStore
}

export function renderWithProviders(
ui: React.ReactElement,
{
preloadedState = {},
// Automatically create a store instance if no store was passed in
store = setupStore(preloadedState),
...renderOptions
}: ExtendedRenderOptions = {}
) {
function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element {
return <Provider store={store}>{children}</Provider>
}
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }
}

使用组件编写集成测试

¥Writing Integration Tests With Components

实际的测试文件应该使用自定义 render 函数来实际渲染我们的 Redux 连接组件。如果我们正在测试的代码涉及发出网络请求,我们还应该配置 MSW 以使用适当的测试数据模拟预期的请求。

¥The actual test files should use the custom render function to actually render our Redux-connected components. If the code that we're testing involves making network requests, we should also configure MSW to mock the expected requests with appropriate test data.

features/users/tests/UserDisplay.test.tsx
import React from 'react'
import { http, HttpResponse, delay } from 'msw'
import { setupServer } from 'msw/node'
import { fireEvent, screen } from '@testing-library/react'
// We're using our own custom render function and not RTL's render.
import { renderWithProviders } from '../../../utils/test-utils'
import UserDisplay from '../UserDisplay'

// We use msw to intercept the network request during the test,
// and return the response 'John Smith' after 150ms
// when receiving a get request to the `/api/user` endpoint
export const handlers = [
http.get('/api/user', async () => {
await delay(150)
return HttpResponse.json('John Smith')
})
]

const server = setupServer(...handlers)

// Enable API mocking before tests.
beforeAll(() => server.listen())

// Reset any runtime request handlers we may add during the tests.
afterEach(() => server.resetHandlers())

// Disable API mocking after the tests are done.
afterAll(() => server.close())

test('fetches & receives a user after clicking the fetch user button', async () => {
renderWithProviders(<UserDisplay />)

// should show no user initially, and not be fetching a user
expect(screen.getByText(/no user/i)).toBeInTheDocument()
expect(screen.queryByText(/Fetching user\.\.\./i)).not.toBeInTheDocument()

// after clicking the 'Fetch user' button, it should now show that it is fetching the user
fireEvent.click(screen.getByRole('button', { name: /Fetch user/i }))
expect(screen.getByText(/no user/i)).toBeInTheDocument()

// after some time, the user should be received
expect(await screen.findByText(/John Smith/i)).toBeInTheDocument()
expect(screen.queryByText(/no user/i)).not.toBeInTheDocument()
expect(screen.queryByText(/Fetching user\.\.\./i)).not.toBeInTheDocument()
})

在这个测试中,我们完全避免了直接测试任何 Redux 代码,将其视为实现细节。因此,我们可以自由地重构实现,同时我们的测试将继续通过并避免漏报(尽管应用仍然按照我们希望的方式运行,但测试失败)。我们可能会更改状态结构,将切片转换为使用 RTK 查询,或者完全删除 Redux,但我们的测试仍然会通过。我们非常有信心,如果我们更改了一些代码并且测试报告失败,那么我们的应用就真的坏了。

¥In this test, we have completely avoided testing any Redux code directly, treating it as an implementation detail. As a result, we are free to re-factor the implementation, while our tests will continue to pass and avoid false negatives (tests that fail despite the app still behaving how we want it to). We might change our state structure, convert our slice to use RTK-Query, or remove Redux entirely, and our tests will still pass. We have a strong degree of confidence that if we change some code and our tests report a failure, then our app really is broken.

准备初始测试状态

¥Preparing Initial Test State

许多测试要求在渲染组件之前 Redux 存储中已经存在某些状态。使用自定义渲染函数,你可以通过多种不同的方式来实现这一点。

¥Many tests require that certain pieces of state already exist in the Redux store before the component is rendered. With the custom render function, there are a couple different ways you can do that.

一种选择是将 preloadedState 参数传递给自定义渲染函数:

¥One option is to pass a preloadedState argument in to the custom render function:

TodoList.test.tsx
test('Uses preloaded state to render', () => {
const initialTodos = [{ id: 5, text: 'Buy Milk', completed: false }]

const { getByText } = renderWithProviders(<TodoList />, {
preloadedState: {
todos: initialTodos
}
})
})

另一种选择是首先创建一个自定义 Redux 存储并分派一些操作来构建所需的状态,然后传入该特定存储实例:

¥Another option is to create a custom Redux store first and dispatch some actions to build up the desired state, then pass in that specific store instance:

TodoList.test.tsx
test('Sets up initial state state with actions', () => {
const store = setupStore()
store.dispatch(todoAdded('Buy milk'))

const { getByText } = renderWithProviders(<TodoList />, { store })
})

你还可以从自定义渲染函数返回的对象中提取 store,并在稍后作为测试的一部分分派更多操作。

¥You can also extract store from the object returned by the custom render function, and dispatch more actions later as part of the test.

单元测试单个函数

¥Unit Testing Individual Functions

虽然我们建议默认使用集成测试,因为它们一起运行所有 Redux 逻辑,但有时你可能还想为各个函数编写单元测试。

¥While we recommend using integration tests by default, since they exercise all the Redux logic working together, you may sometimes want to write unit tests for individual functions as well.

Reducer

reducer 是纯函数,在将操作应用于前一个状态后返回新状态。在大多数情况下,reducer 是一个不需要显式测试的实现细节。但是,如果你的 reducer 包含特别复杂的逻辑,你希望有信心对其进行单元测试,则可以轻松测试 reducer。

¥Reducers are pure functions that return the new state after applying the action to the previous state. In the majority of cases, the reducer is an implementation detail that does not need explicit tests. However, if your reducer contains particularly complex logic that you would like the confidence of having unit tests for, reducers can be easily tested.

因为 reducer 是纯函数,所以测试它们应该很简单。使用特定输入 stateaction 调用 reducer,并断言结果状态符合预期。

¥Because reducers are pure functions, so testing them should be straightforward. Call the reducer with a specific input state and action, and assert that the result state matches expectations.

示例

¥Example

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

export type Todo = {
id: number
text: string
completed: boolean
}

const initialState: Todo[] = [{ text: 'Use Redux', completed: false, id: 0 }]

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action: PayloadAction<string>) {
state.push({
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
completed: false,
text: action.payload
})
}
}
})

export const { todoAdded } = todosSlice.actions

export default todosSlice.reducer

可以这样测试:

¥can be tested like:

import reducer, { todoAdded, Todo } from './todosSlice'

test('should return the initial state', () => {
expect(reducer(undefined, { type: 'unknown' })).toEqual([
{ text: 'Use Redux', completed: false, id: 0 }
])
})

test('should handle a todo being added to an empty list', () => {
const previousState: Todo[] = []

expect(reducer(previousState, todoAdded('Run the tests'))).toEqual([
{ text: 'Run the tests', completed: false, id: 0 }
])
})

test('should handle a todo being added to an existing list', () => {
const previousState: Todo[] = [
{ text: 'Run the tests', completed: true, id: 0 }
]

expect(reducer(previousState, todoAdded('Use Redux'))).toEqual([
{ text: 'Run the tests', completed: true, id: 0 },
{ text: 'Use Redux', completed: false, id: 1 }
])
})

选择器

¥Selectors

选择器通常也是纯函数,因此可以使用与 reducer 相同的基本方法进行测试:设置初始值,使用这些输入调用选择器函数,并断言结果与预期输出匹配。

¥Selectors are also generally pure functions, and thus can be tested using the same basic approach as reducers: set up an initial value, call the selector function with those inputs, and assert that the result matches the expected output.

但是,自 大多数选择器都会被记忆以记住他们最后的输入 起,你可能需要注意选择器返回缓存值的情况,而你希望它根据测试中使用的位置生成新值。

¥However, since most selectors are memoized to remember their last inputs, you may need to watch for cases where a selector is returning a cached value when you expected it to generate a new one depending on where it's being used in the test.

动作创作者和 thunk

¥Action Creators & Thunks

在 Redux 中,动作创建者是返回普通对象的函数。我们的建议是不要手动编写动作创建器,而是让它们由 createSlice 自动生成,或者通过 createAction@reduxjs/toolkit 创建。因此,你不应该觉得需要自己测试操作创建者(Redux Toolkit 维护人员已经为你完成了这项工作!)。

¥In Redux, action creators are functions which return plain objects. Our recommendation is not to write action creators manually, but instead have them generated automatically by createSlice, or created via createAction from @reduxjs/toolkit. As such, you should not feel the need to test action creators by themselves (the Redux Toolkit maintainers have already done that for you!).

操作创建者的返回值被视为应用中的实现细节,并且在遵循集成测试风格时,不需要显式测试。

¥The return value of action creators is considered an implementation detail within your application, and when following an integration testing style, do not need explicit tests.

同样,对于使用 Redux Thunk 的 thunk,我们建议不要手动编写它们,而是使用 @reduxjs/toolkit 中的 createAsyncThunk。thunk 会根据 thunk 的生命周期为你分派适当的 pendingfulfilledrejected 操作类型。

¥Similarly for thunks using Redux Thunk, our recommendation is not to write them manually, but instead use createAsyncThunk from @reduxjs/toolkit. The thunk handles dispatching the appropriate pending, fulfilled and rejected action types for you based on the lifecycle of the thunk.

我们认为 thunk 行为是应用的实现细节,并建议通过使用它测试组件组(或整个应用)来覆盖它,而不是单独测试 thunk。

¥We consider thunk behavior to be an implementation detail of the application, and recommend that it be covered by testing the group of components (or whole app) using it, rather than testing the thunk in isolation.

我们的建议是使用 mswmiragejsjest-fetch-mockfetch-mock 或类似工具在 fetch/xhr 级别模拟异步请求。通过在此级别模拟请求,测试中无需更改任何 thunk 逻辑 - thunk 仍然尝试发出 "real" 异步请求,但它只是被拦截。有关测试内部包含 thunk 行为的组件的示例,请参阅 "集成测试" 示例

¥Our recommendation is to mock async requests at the fetch/xhr level using tools like msw, miragejs, jest-fetch-mock, fetch-mock, or similar. By mocking requests at this level, none of the thunk logic has to change in a test - the thunk still tries to make a "real" async request, it just gets intercepted. See the "Integration Test" example for an example of testing a component which internally includes the behavior of a thunk.

信息

如果你愿意或需要为你的操作创建者或 thunk 编写单元测试,请参阅 Redux Toolkit 用于 createActioncreateAsyncThunk 的测试。

¥If you prefer, or are otherwise required to write unit tests for your action creators or thunks, refer to the tests that Redux Toolkit uses for createAction and createAsyncThunk.

中间件

¥Middleware

中间件函数在 Redux 中封装 dispatch 调用的行为,因此为了测试此修改后的行为,我们需要模拟 dispatch 调用的行为。

¥Middleware functions wrap behavior of dispatch calls in Redux, so to test this modified behavior we need to mock the behavior of the dispatch call.

示例

¥Example

首先,我们需要一个中间件函数。这与真正的 redux-thunk 相似。

¥First, we'll need a middleware function. This is similar to the real redux-thunk.

const thunkMiddleware =
({ dispatch, getState }) =>
next =>
action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}

return next(action)
}

我们需要创建假 getStatedispatchnext 函数。我们使用 jest.fn() 来创建存根,但对于其他测试框架,你可能会使用 Sinon

¥We need to create fake getState, dispatch, and next functions. We use jest.fn() to create stubs, but with other test frameworks you would likely use Sinon.

invoke 函数以与 Redux 相同的方式运行我们的中间件。

¥The invoke function runs our middleware in the same way Redux does.

const create = () => {
const store = {
getState: jest.fn(() => ({})),
dispatch: jest.fn()
}
const next = jest.fn()

const invoke = action => thunkMiddleware(store)(next)(action)

return { store, next, invoke }
}

我们测试我们的中间件是否在正确的时间调用 getStatedispatchnext 函数。

¥We test that our middleware is calling the getState, dispatch, and next functions at the right time.

test('passes through non-function action', () => {
const { next, invoke } = create()
const action = { type: 'TEST' }
invoke(action)
expect(next).toHaveBeenCalledWith(action)
})

test('calls the function', () => {
const { invoke } = create()
const fn = jest.fn()
invoke(fn)
expect(fn).toHaveBeenCalled()
})

test('passes dispatch and getState', () => {
const { store, invoke } = create()
invoke((dispatch, getState) => {
dispatch('TEST DISPATCH')
getState()
})
expect(store.dispatch).toHaveBeenCalledWith('TEST DISPATCH')
expect(store.getState).toHaveBeenCalled()
})

在某些情况下,你需要修改 create 函数以使用 getStatenext 的不同模拟实现。

¥In some cases, you will need to modify the create function to use different mock implementations of getState and next.

更多信息

¥Further Information

  • React 测试库:React 测试库是一个用于测试 React 组件的非常轻量级的解决方案。它在 react-dom 和 react-dom/test-utils 之上提供了轻量级的实用函数,以鼓励更好的测试实践。其主要指导原则是:"你的测试越接近软件的使用方式,它们就越能给你带来信心。"

    ¥React Testing Library: React Testing Library is a very light-weight solution for testing React components. It provides light utility functions on top of react-dom and react-dom/test-utils, in a way that encourages better testing practices. Its primary guiding principle is: "The more your tests resemble the way your software is used, the more confidence they can give you."

  • 博客上的答案:Redux 测试方法的演变:Mark Erikson 关于 Redux 测试如何从 'isolation' 发展到 'integration' 的想法。

    ¥Blogged Answers: The Evolution of Redux Testing Approaches: Mark Erikson's thoughts on how Redux testing has evolved from 'isolation' to 'integration'.

  • 测试实现细节:博客文章作者:Kent C.多兹解释了为什么他建议避免测试实现细节。

    ¥Testing Implementation Details: Blog post by Kent C. Dodds on why he recommends to avoid testing implementation details.