我是 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 方法仍然執行兩次 - 第一次使用空書籍數組,第二次使用獲取的書籍數組。
P粉9860280392024-02-27 00:23:34
最終的根本原因是我的後端和前端之間的回應資料映射錯誤。
我對取得圖書端點的 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(), }));
一切都如預期進行。 :)