我是 React 和 Jest 的新手,到目前为止几乎所有事情都在挣扎。我正在尝试按照我找到的教程进行操作。
这是一个简单的书店 React 前端应用程序。到目前为止,我已经创建了一个简单的布局组件,然后在 BookContainer 组件内创建了 BookList 组件,其中包含已获取的书籍列表。然后每本书都有一个 BookListItem 组件。
然后我有简单的 BookService 和 getAllBooks 用于从后端的 Rest Api 获取书籍。此外,我还有一个简单的 BookReducer、BookSelector 和 BookAction,它们都处理 Redux 存储中的保存和获取。
我正在使用 redux、react-hooks、redux 工具包、jest 和 javascript。
当我在网络浏览器中运行它时,一切正常,书籍被获取,保存到商店中,然后呈现在 BookContainer 组件中。
现在我正在尝试为此 BookContainer 组件添加一个简单的单元测试,并寻求帮助。
我希望此单元测试检查 BookList 组件是否已渲染 (haveBeenCalledWith),即我传递到渲染方法中的书籍列表。
我还想模拟 BookAction,以返回我传递给渲染的书籍列表。这正是我现在正在努力解决的问题。
这是我的 BookContainer 组件:
import React, { useEffect } from 'react';
import { Box } from '@mui/material';
import { useDispatch, useSelector } from 'react-redux';
import getBooksAction from '../../modules/book/BookAction';
import BookFilter from './BookFilter';
import styles from './BookStyles.module.css';
import { getBooksSelector } from '../../modules/book/BookSelector';
import BookList from './BookList';
const BookContainer = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getBooksAction());
}, [dispatch]);
const booksResponse = useSelector(getBooksSelector);
if (booksResponse && booksResponse.books) {
return (
<Box className={styles.bookContainer}>
<BookFilter />
<Box className={styles.bookList}>
<BookList books={booksResponse.books} />
</Box>
</Box>
);
}
return <BookList books={[]} />;
}
export default BookContainer;
这是我的 BookList 组件:
import { Box } from '@mui/material';
import Proptypes from 'prop-types';
import React from 'react';
import styles from './BookStyles.module.css';
import BookListItem from './BookListItem';
const propTypes = {
books: Proptypes.arrayOf(
Proptypes.shape({
id: Proptypes.number.isRequired,
title: Proptypes.string.isRequired,
description: Proptypes.string.isRequired,
author: Proptypes.string.isRequired,
releaseYear: Proptypes.number.isRequired,
})
).isRequired,
};
const BookList = ({books}) => {
return (
<Box className={styles.bookList} ml={5}>
{books.map((book) => {
return (
<BookListItem book={book} key={book.id} />
);
})}
</Box>
);
}
BookList.propTypes = propTypes;
export default BookList;
这是我的 BookAction:
import getBooksService from "./BookService";
const getBooksAction = () => async (dispatch) => {
try {
// const books = await getBooksService();
// dispatch({
// type: 'BOOKS_RESPONSE',
// payload: books.data
// });
return getBooksService().then(res => {
dispatch({
type: 'BOOKS_RESPONSE',
payload: res.data
});
});
}
catch(error) {
console.log(error);
}
};
export default getBooksAction;
这是我的 BookContainer.test.jsx:
import React from "react";
import { renderWithRedux } from '../../../helpers/test_helpers/TestSetupProvider';
import BookContainer from "../BookContainer";
import BookList from "../BookList";
import getBooksAction from "../../../modules/book/BookAction";
import { bookContainerStateWithData } from '../../../helpers/test_helpers/TestDataProvider';
// Mocking component
jest.mock("../BookList", () => jest.fn());
jest.mock("../../../modules/book/BookAction", () => ({
getBooksAction: jest.fn(),
}));
describe("BookContainer", () => {
it("should render without error", () => {
const books = bookContainerStateWithData.initialState.bookReducer.books;
// Mocking component
BookList.mockImplementation(() => <div>mock booklist comp</div>);
// Mocking actions
getBooksAction.mockImplementation(() => (dispatch) => {
dispatch({
type: "BOOKS_RESPONSE",
payload: books,
});
});
renderWithRedux(<BookContainer />, {});
// Asserting BookList was called (was correctly mocked) in BookContainer
expect(BookList).toHaveBeenLastCalledWith({ books }, {});
});
});
这是我在测试中使用的 bookContainerStateWithData 的 TestDataProvider:
const getBooksActionData = [
{
id: 1,
title: 'test title',
description: 'test description',
author: 'test author',
releaseYear: 1951
}
];
const getBooksReducerData = {
books: getBooksActionData
};
const bookContainerStateWithData = {
initialState: {
bookReducer: {
...getBooksReducerData
}
}
};
export {
bookContainerStateWithData
};
这是我在测试中使用的来自 TestSetupProvider 的 renderWithRedux() 辅助方法:
import { createSoteWithMiddleware } from '../ReduxStoreHelper';
import React from 'react';
import { Provider } from 'react-redux';
import reducers from '../../modules';
const renderWithRedux = (
ui,
{
initialState,
store = createSoteWithMiddleware(reducers, initialState)
}
) => ({
...render(
<Provider store={store}>{ui}</Provider>
)
});
这是我的 ReduxStoreHelper,它提供了 TestSetupProvider 中使用的 createSoteWithMiddleware():
import reduxThunk from 'redux-thunk';
import { legacy_createStore as createStore, applyMiddleware } from "redux";
import reducers from '../modules';
const createSoteWithMiddleware = applyMiddleware(reduxThunk)(createStore);
export {
createSoteWithMiddleware
}
以及我当前收到的错误消息:
BookContainer › should render without error TypeError: _BookAction.default.mockImplementation is not a function
在 BookContainer 单元测试中的这一行:
getBooksAction.mockImplementation(() => (dispatch) => {
感谢您的任何帮助或建议。我一直在寻找类似的问题和解决方案,但到目前为止还没有成功。
如果我将 __esModule: true 添加到 getBooksAction 的笑话模拟中,如下所示:
jest.mock("../../../modules/book/BookAction", () => ({
__esModule: true,
getBooksAction: jest.fn(),
}));
那么错误消息就不同了:
TypeError: Cannot read properties of undefined (reading 'mockImplementation')
如果我在玩笑模拟中将 getBooksAction 键更改为默认值,如下所示:
jest.mock("../../../modules/book/BookAction", () => ({
__esModule: true,
default: jest.fn(),
}));
然后不再有类型错误,而是断言错误(更接近一点):
- Expected
+ Received
Object {
- "books": Array [
- Object {
- "author": "test author",
- "description": "test description",
- "id": 1,
- "releaseYear": 1951,
- "title": "test title",
- },
- ],
+ "books": Array [],
},
{},
Number of calls: 1
所以现在返回了空的书籍数组。那么如何更改模拟来分派给定的书籍数组?
我想我已经找到问题的根本原因了。创建和渲染 BookContainer 时,会连续多次获取书籍。前两个返回空的书籍数组。从第三次开始,返回获取到的 books 数组。我通过在 useEffect 之后将控制台日志添加到 BookContainer 来知道这一点:
const booksResponse = useSelector(getBooksSelector); console.log(booksResponse);
它应该连续被调用很多次吗?难道不应该只是一次正确获取书籍数组的调用吗?造成这种行为的原因是什么,是否是我的代码在其他地方出现了错误?
顺便说一句,这也是我在 BookContainer 组件中出现这个令人讨厌的 IF 语句的原因。尽管在教程中没有,但一切都按预期工作。每次渲染 BookContainer 时,请求/操作似乎都会加倍......
我在索引文件中使用了 StrictMode。删除它后,双倍的请求消失了,BookContainer 中的 useEffect() 现在只执行一次。但 BookContainer 的 render 方法仍然执行两次 - 第一次使用空书籍数组,第二次使用获取的书籍数组。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号
最终的根本原因是我的后端和前端之间的响应数据映射错误。
我对获取图书端点的 API 响应是这样的:
{ "books": [...] }所以基本上它不是一个 json 数组,而是一个内部有数组的 json 对象。正如 API 响应的良好实践所说,要更加灵活。
但是,在我的前端,我编写的代码基本上错误地假设 api 响应只是 BookList 中的 json 数组:
const propTypes = { books: Proptypes.arrayOf( Proptypes.shape({ id: Proptypes.number.isRequired, title: Proptypes.string.isRequired, description: Proptypes.string.isRequired, author: Proptypes.string.isRequired, releaseYear: Proptypes.number.isRequired, }) ).isRequired, };将其更改为:
const propTypes = { booksResponse: Proptypes.shape({ books: Proptypes.arrayOf( Proptypes.shape({ id: Proptypes.number.isRequired, title: Proptypes.string.isRequired, description: Proptypes.string.isRequired, author: Proptypes.string.isRequired, releaseYear: Proptypes.number.isRequired, }) ).isRequired, }) };然后在 BookList 组件中进一步适应此更改:
const BookList = ({booksResponse}) => { return ( <Box className={styles.bookList} ml={5}> {booksResponse.books.map((book) => { return ( <BookListItem book={book} key={book.id} /> ); })} </Box> ); }最后也在单元测试中:
expect(BookList).toHaveBeenLastCalledWith({ booksResponse: books }, {});并且 getBooksAction 模拟不需要任何默认值或 __esModule:
jest.mock("../../../modules/book/BookAction", () => ({ getBooksAction: jest.fn(), }));一切都按预期进行。 :)