我是 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 方法仍然执行两次 - 第一次使用空书籍数组,第二次使用获取的书籍数组。
最终的根本原因是我的后端和前端之间的响应数据映射错误。
我对获取图书端点的 API 响应是这样的:
所以基本上它不是一个 json 数组,而是一个内部有数组的 json 对象。正如 API 响应的良好实践所说,要更加灵活。
但是,在我的前端,我编写的代码基本上错误地假设 api 响应只是 BookList 中的 json 数组:
将其更改为:
然后在 BookList 组件中进一步适应此更改:
最后也在单元测试中:
并且 getBooksAction 模拟不需要任何默认值或 __esModule:
一切都按预期进行。 :)