Heim > Fragen und Antworten > Hauptteil
Ich bin neu bei React and Jest und habe bisher mit fast allem zu kämpfen. Ich versuche, einem Tutorial zu folgen, das ich gefunden habe.
Dies ist eine einfache React-Frontend-Anwendung für Buchhandlungen. Bisher habe ich eine einfache Layout-Komponente erstellt und dann innerhalb der BookContainer-Komponente eine BookList-Komponente erstellt, die die abgerufene Liste der Bücher enthält. Dann verfügt jedes Buch über eine BookListItem-Komponente.
Dann habe ich den einfachen BookService und getAllBooks, um Bücher von der Rest-API im Backend abzurufen. Darüber hinaus habe ich einen einfachen BookReducer, BookSelector und BookAction, die alle das Speichern und Abrufen aus dem Redux-Store übernehmen.
Ich verwende Redux, React-Hooks, Redux Toolkit, Jest und Javascript.
Wenn ich es in einem Webbrowser ausführe, funktioniert alles einwandfrei, das Buch wird abgerufen, im Store gespeichert und dann in der BookContainer-Komponente gerendert.
Jetzt versuche ich, einen einfachen Komponententest für diese BookContainer-Komponente hinzuzufügen und suche nach Hilfe.
Ich möchte, dass dieser Unit-Test prüft, ob die BookList-Komponente gerendert wurde (haveBeenCalledWith), d. h. die Liste der Bücher, die ich an die Render-Methode übergeben habe.
Ich möchte auch eine BookAction verspotten, die die Liste der Bücher zurückgibt, die ich zum Rendern übergebe. Das ist genau das, womit ich gerade zu kämpfen habe.
Das ist meine BookContainer-Komponente:
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;
Das ist meine BookList-Komponente:
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;
Das ist meine 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;
Das ist mein 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 }, {}); }); });
Dies ist der TestDataProvider von bookContainerStateWithData, den ich in meinen Tests verwende:
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 };
Dies ist die renderWithRedux()-Hilfsmethode von TestSetupProvider, die ich in meinen Tests verwende:
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> ) });
Dies ist mein ReduxStoreHelper, der createSoteWithMiddleware() bereitstellt, das in TestSetupProvider verwendet wird:
import reduxThunk from 'redux-thunk'; import { legacy_createStore as createStore, applyMiddleware } from "redux"; import reducers from '../modules'; const createSoteWithMiddleware = applyMiddleware(reduxThunk)(createStore); export { createSoteWithMiddleware }
Und die Fehlermeldung, die ich derzeit erhalte:
BookContainer › should render without error TypeError: _BookAction.default.mockImplementation is not a function
Diese Zeile im BookContainer-Unit-Test:
getBooksAction.mockImplementation(() => (dispatch) => {
Vielen Dank für jede Hilfe oder jeden Rat. Ich habe nach ähnlichen Problemen und Lösungen gesucht, aber bisher ohne Erfolg.
Wenn ich __esModule: true
zum Witz-Mock von getBooksAction hinzufüge, so:
jest.mock("../../../modules/book/BookAction", () => ({ __esModule: true, getBooksAction: jest.fn(), }));
Dann ist die Fehlermeldung eine andere:
TypeError: Cannot read properties of undefined (reading 'mockImplementation')
Wenn ich in einer Scherzsimulation wie dieser die getBooksAction-Taste auf „Standard“ ändere:
jest.mock("../../../modules/book/BookAction", () => ({ __esModule: true, default: jest.fn(), }));
Dann liegt kein Typfehler mehr vor, sondern ein Assertionsfehler (etwas näher):
- 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
Jetzt wird also ein leeres Books-Array zurückgegeben. Wie kann ich also die Simulation ändern, um die angegebene Reihe von Büchern zu versenden?
Ich glaube, ich habe die Grundursache des Problems gefunden. Wenn ein BookContainer erstellt und gerendert wird, werden Bücher mehrmals hintereinander abgerufen. Die ersten beiden geben leere Bücher-Arrays zurück. Geben Sie ab dem dritten Mal das erhaltene Bücher-Array zurück. Ich weiß das, indem ich das Konsolenprotokoll nach useEffect:
zum BookContainer hinzufügeconst booksResponse = useSelector(getBooksSelector); console.log(booksResponse);
Soll es mehrmals hintereinander aufgerufen werden? Sollte es nicht nur ein einziger Aufruf sein, um die Reihe der Bücher korrekt abzurufen? Was verursacht dieses Verhalten? Stimmt an einer anderen Stelle in meinem Code etwas nicht?
Das ist übrigens auch der Grund, warum ich diese lästige IF-Anweisung in der BookContainer-Komponente habe. Obwohl nicht im Tutorial beschrieben, funktioniert alles wie erwartet. Die Anfragen/Vorgänge scheinen sich jedes Mal zu verdoppeln, wenn der BookContainer gerendert wird ...
Ich habe StrictMode in der Indexdatei verwendet. Nach dem Entfernen ist die doppelte Anfrage verschwunden und useEffect() in BookContainer wird nur noch einmal ausgeführt. Aber die Render-Methode des BookContainers wird immer noch zweimal ausgeführt – das erste Mal mit dem leeren Bücher-Array und das zweite Mal mit dem abgerufenen Bücher-Array.
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(), }));
一切都按预期进行。 :)