编写测试
- 使用 Redux 测试应用程序的推荐实践
- 测试配置和设置的示例
指导原则
测试 Redux 逻辑的指导原则与 React Testing Library 非常相似
你的测试越接近软件的使用方式,它们就能带给你越多的信心。 - Kent C. Dodds
因为你编写的 Redux 代码大部分是函数,而且很多是纯函数,所以它们很容易在不进行模拟的情况下进行测试。但是,你应该考虑你的 Redux 代码中的每一部分是否都需要自己的专用测试。在大多数情况下,最终用户并不知道也不关心应用程序中是否使用了 Redux。因此,在许多情况下,Redux 代码可以被视为应用程序的实现细节,无需对 Redux 代码进行显式测试。
我们对使用 Redux 测试应用程序的一般建议是
- 优先编写将所有内容协同工作的集成测试。对于使用 Redux 的 React 应用程序,使用真实的存储实例渲染一个
<Provider>
,并将要测试的组件包装起来。与正在测试的页面的交互应该使用真实的 Redux 逻辑,并模拟 API 调用,以便应用程序代码无需更改,并断言 UI 更新是否正确。 - 如果需要,对纯函数(例如特别复杂的 reducer 或 selector)使用基本的单元测试。但是,在许多情况下,这些只是由集成测试覆盖的实现细节。
- 不要尝试模拟 selector 函数或 React-Redux hook!模拟库中的导入很脆弱,而且不能让你确信你的实际应用程序代码是否正常工作。
有关我们推荐集成式测试的原因的背景信息,请参阅
- Kent C Dodds: 测试实现细节: 关于他为什么建议避免测试实现细节的想法。
- Mark Erikson: 博客答案:Redux 测试方法的演变: 关于 Redux 测试如何随着时间的推移从“隔离”演变到“集成”的想法。
设置测试环境
测试运行器
Redux 可以使用任何测试运行器进行测试,因为它只是普通的 JavaScript。一个常见的选项是 Jest,这是一个广泛使用的测试运行器,它与 Create-React-App 一起提供,并被 Redux 库仓库使用。如果你使用 Vite 构建你的项目,你可能正在使用 Vitest 作为你的测试运行器。
通常,你的测试运行器需要配置为编译 JavaScript/TypeScript 语法。如果你要测试 UI 组件,你可能需要配置测试运行器以使用 JSDOM 来提供模拟 DOM 环境。
本页面的示例将假设你使用的是 Jest,但无论你使用什么测试运行器,相同的模式都适用。
查看这些资源以获取典型的测试运行器配置说明
UI 和网络测试工具
Redux 团队建议使用 React Testing Library (RTL) 来测试连接到 Redux 的 React 组件。React Testing Library 是一个简单而完整的 React DOM 测试实用程序,它鼓励良好的测试实践。它使用 ReactDOM 的 render
函数和来自 react-dom/tests-utils 的 act
。(Testing Library 工具家族还包括 许多其他流行框架的适配器。)
我们还建议使用 Mock Service Worker (MSW) 来模拟网络请求,因为这意味着在编写测试时,您的应用程序逻辑不需要更改或模拟。
- DOM/React Testing Library
- Mock Service Worker
集成测试连接的组件和 Redux 逻辑
我们建议通过包含所有协同工作的集成测试来测试 Redux 连接的 React 组件,并使用断言来验证当用户以特定方式与应用程序交互时,应用程序的行为是否符合您的预期。
示例应用程序代码
考虑以下 userSlice
切片、存储和 App
组件
- TypeScript
- JavaScript
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
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
export const fetchUser = createAsyncThunk('user/fetchUser', async () => {
const response = await userAPI.fetchUser()
return response.data
})
const initialState = {
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 => state.user.name
export const selectUserFetchStatus = state => state.user.status
export default userSlice.reducer
- TypeScript
- JavaScript
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']
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) {
return configureStore({
reducer: rootReducer,
preloadedState
})
}
- TypeScript
- JavaScript
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>()
import { useDispatch, useSelector } from 'react-redux'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes()
export const useAppSelector = useSelector.withTypes()
- TypeScript
- JavaScript
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>
)
}
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 和 selector。所有这些都可以通过编写集成测试来测试,并牢记以下几点
- 首次加载应用程序时,应该还没有用户 - 屏幕上应该显示“没有用户”。
- 单击“获取用户”按钮后,我们希望它开始获取用户。屏幕上应该显示“正在获取用户...”。
- 一段时间后,应该收到用户。我们不应该再看到“正在获取用户...”,而是应该根据我们 API 的响应看到预期的用户名。
将我们的测试重点放在上述内容的整体上,我们可以避免尽可能多地模拟应用程序。我们还将确信,当以我们期望用户使用应用程序的方式与应用程序交互时,应用程序的关键行为将按预期执行。
为了测试组件,我们将其render
到 DOM 中,并断言应用程序以我们期望用户使用应用程序的方式响应交互。
设置可重用测试渲染函数
React Testing Library 的render
函数接受 React 元素树并渲染这些组件。就像在真实应用程序中一样,任何与 Redux 连接的组件都需要一个 React-Redux <Provider>
组件包裹在它们周围,并设置和提供一个真实的 Redux 存储。
此外,测试代码应该为每个测试创建一个单独的 Redux 存储实例,而不是重复使用同一个存储实例并重置其状态。这确保了没有值意外泄漏到测试之间。
与其在每个测试中复制粘贴相同的存储创建和Provider
设置,我们可以使用render
函数中的wrapper
选项,导出我们自己的自定义renderWithProviders
函数,该函数创建一个新的 Redux 存储并渲染一个<Provider>
,如React Testing Library 的设置文档中所述。
自定义渲染函数应该让我们
- 每次调用时创建一个新的 Redux 存储实例,并提供一个可选的
preloadedState
值,该值可用于初始值 - 或者传入一个已经创建的 Redux 存储实例
- 将其他选项传递给 RTL 的原始
render
函数 - 自动将被测试的组件包装在
<Provider store={store}>
中 - 返回 store 实例,以防测试需要分派更多操作或检查状态
典型的自定义渲染函数设置可能如下所示
- TypeScript
- JavaScript
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 })
}
}
import React from 'react'
import { render } from '@testing-library/react'
import { Provider } from 'react-redux'
import { setupStore } from '../app/store'
export function renderWithProviders(ui, extendedRenderOptions = {}) {
const {
preloadedState = {},
// Automatically create a store instance if no store was passed in
store = setupStore(preloadedState),
...renderOptions
} = extendedRenderOptions
const Wrapper = ({ children }) => (
<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 来创建 store。创建一个可重用的 setupStore
函数来执行 store 创建,并使用正确的选项和配置,然后在自定义渲染函数中使用它可能会有所帮助。
- TypeScript
- JavaScript
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']
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 => {
return configureStore({
reducer: rootReducer,
preloadedState
})
}
然后,在测试工具文件中使用 setupStore
,而不是再次调用 configureStore
- TypeScript
- JavaScript
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 }) }
}
import React from 'react'
import { render } from '@testing-library/react'
import { Provider } from 'react-redux'
import { setupStore } from '../app/store'
export function renderWithProviders(
ui,
{
preloadedState = {},
// Automatically create a store instance if no store was passed in
store = setupStore(preloadedState),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return <Provider store={store}>{children}</Provider>
}
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }
}
使用组件编写集成测试
实际的测试文件应该使用自定义的 render
函数来实际渲染我们的 Redux 连接组件。如果我们正在测试的代码涉及进行网络请求,我们还应该配置 MSW 以使用适当的测试数据模拟预期的请求。
- TypeScript
- JavaScript
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()
})
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-Query,或者完全删除 Redux,我们的测试仍然会通过。我们有充分的信心,如果我们更改了一些代码并且我们的测试报告失败,那么我们的应用程序确实是坏了。
准备初始测试状态
许多测试要求在渲染组件之前,Redux store 中已经存在某些状态片段。使用自定义渲染函数,您可以通过几种不同的方式做到这一点。
一种选择是在自定义渲染函数中传递 preloadedState
参数
test('Uses preloaded state to render', () => {
const initialTodos = [{ id: 5, text: 'Buy Milk', completed: false }]
const { getByText } = renderWithProviders(<TodoList />, {
preloadedState: {
todos: initialTodos
}
})
})
另一种选择是首先创建一个自定义 Redux store 并分派一些操作来构建所需的状态,然后传入该特定 store 实例
test('Sets up initial state state with actions', () => {
const store = setupStore()
store.dispatch(todoAdded('Buy milk'))
const { getByText } = renderWithProviders(<TodoList />, { store })
})
您也可以从自定义渲染函数返回的对象中提取store
,并在测试过程中稍后分派更多操作。
单元测试单个函数
虽然我们建议默认使用集成测试,因为它们会测试所有协同工作的 Redux 逻辑,但您有时可能也希望为单个函数编写单元测试。
Reducers
Reducers 是纯函数,它们在将操作应用于先前状态后返回新的状态。在大多数情况下,reducer 是一个实现细节,不需要显式测试。但是,如果您的 reducer 包含您希望对其进行单元测试以获得信心的特别复杂的逻辑,则可以轻松地测试 reducer。
因为 reducer 是纯函数,所以测试它们应该很简单。使用特定的输入state
和action
调用 reducer,并断言结果状态与预期相符。
示例
- TypeScript
- JavaScript
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
import { createSlice } from '@reduxjs/toolkit'
const initialState = [{ text: 'Use Redux', completed: false, id: 0 }]
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
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
可以像这样测试
- TypeScript
- JavaScript
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 }
])
})
import reducer, { todoAdded } 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 = []
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 = [{ 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
Selectors 通常也是纯函数,因此可以使用与 reducer 相同的基本方法进行测试:设置初始值,使用这些输入调用 selector 函数,并断言结果与预期输出匹配。
但是,由于大多数 selector 都被记忆以记住其最后的输入,您可能需要注意 selector 在您期望它根据其在测试中的使用位置生成新值时返回缓存值的情况。
Action Creators & Thunks
在 Redux 中,action creators 是返回普通对象的函数。我们建议不要手动编写 action creators,而是让它们由createSlice
自动生成,或者通过createAction
从@reduxjs/toolkit
创建。因此,您不应该觉得有必要单独测试 action creators(Redux Toolkit 维护者已经为您完成了!)。
action creators 的返回值被认为是您应用程序中的实现细节,并且在遵循集成测试风格时,不需要显式测试。
类似地,对于使用 Redux Thunk 的 thunk,我们建议不要手动编写它们,而是使用 createAsyncThunk
来自 @reduxjs/toolkit
。thunk 会根据 thunk 的生命周期,自动为您分发适当的 pending
、fulfilled
和 rejected
操作类型。
我们认为 thunk 行为是应用程序的实现细节,建议通过测试使用它的组件组(或整个应用程序)来覆盖它,而不是单独测试 thunk。
我们建议使用 msw
、miragejs
、jest-fetch-mock
、fetch-mock
或类似工具,在 fetch/xhr
级别模拟异步请求。通过在该级别模拟请求,thunk 中的任何逻辑都不需要在测试中更改 - thunk 仍然尝试进行“真实”的异步请求,只是它被拦截了。请参阅 "集成测试"示例,了解测试内部包含 thunk 行为的组件的示例。
如果您愿意,或者出于其他原因需要为您的操作创建者或 thunk 编写单元测试,请参考 Redux Toolkit 用于 createAction
和 createAsyncThunk
的测试。
中间件
中间件函数包装 Redux 中 dispatch
调用的行为,因此要测试这种修改后的行为,我们需要模拟 dispatch
调用的行为。
示例
首先,我们需要一个中间件函数。这类似于真实的 redux-thunk。
const thunkMiddleware =
({ dispatch, getState }) =>
next =>
action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
我们需要创建假的 getState
、dispatch
和 next
函数。我们使用 jest.fn()
来创建存根,但在其他测试框架中,您可能需要使用 Sinon。
invoke 函数以与 Redux 相同的方式运行我们的中间件。
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 }
}
我们测试我们的中间件是否在正确的时间调用了getState
、dispatch
和next
函数。
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
函数以使用getState
和next
的不同模拟实现。
更多信息
- React Testing Library: React Testing Library 是一个非常轻量级的 React 组件测试解决方案。它在 react-dom 和 react-dom/test-utils 之上提供了轻量级的实用程序函数,以鼓励更好的测试实践。它的主要指导原则为:“测试越接近软件的使用方式,它们就能带给您越多的信心。”
- 博客解答:Redux 测试方法的演变: Mark Erikson 关于 Redux 测试如何从“隔离”演变到“集成”的思考。
- 测试实现细节: Kent C. Dodds 关于为什么他建议避免测试实现细节的博客文章。