Rumah > Soal Jawab > teks badan
Saya baru mengenali React and Jest dan bergelut dengan hampir semua perkara setakat ini. Saya cuba mengikuti tutorial yang saya dapati.
Ini adalah aplikasi bahagian hadapan kedai buku React yang ringkas. Setakat ini saya telah mencipta komponen reka letak yang mudah dan kemudian mencipta komponen BookList dalam komponen BookContainer yang mengandungi senarai buku yang diambil. Kemudian setiap buku mempunyai komponen BookListItem.
Kemudian saya mempunyai BookService dan dapatkanAllBooks yang ringkas untuk mendapatkan buku daripada Rest Api di bahagian belakang. Selain itu, saya mempunyai BookReducer, BookSelector dan BookAction ringkas yang semuanya mengendalikan penjimatan dan perolehan daripada kedai Redux.
Saya menggunakan redux, react-hooks, redux toolkit, jest dan javascript.
Apabila saya menjalankannya dalam penyemak imbas web, semuanya berfungsi dengan baik, buku itu diambil, disimpan ke kedai dan kemudian dipaparkan dalam komponen BookContainer.
Kini saya cuba menambah ujian unit mudah untuk komponen BookContainer ini dan mencari bantuan.
Saya mahu ujian unit ini menyemak sama ada komponen BookList telah dipaparkan (haveBeenCalledWith), iaitu senarai buku yang saya hantar ke dalam kaedah render.
Saya juga ingin mengejek BookAction yang memulangkan senarai buku yang saya lulus untuk diberikan. Inilah yang saya sedang bergelut sekarang.
Ini adalah komponen BookContainer saya:
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;
Ini ialah komponen Senarai Buku saya:
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;
Ini adalah BookAction saya:
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;
Ini adalah BookContainer.test.jsx saya:
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 }, {}); }); });
Ini ialah TestDataProvider bookContainerStateWithData yang saya gunakan dalam ujian saya:
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 };
Ini ialah kaedah pembantu renderWithRedux() daripada TestSetupProvider yang saya gunakan dalam ujian saya:
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> ) });
Ini ialah ReduxStoreHelper saya yang menyediakan createSoteWithMiddleware() yang digunakan dalam TestSetupProvider:
import reduxThunk from 'redux-thunk'; import { legacy_createStore as createStore, applyMiddleware } from "redux"; import reducers from '../modules'; const createSoteWithMiddleware = applyMiddleware(reduxThunk)(createStore); export { createSoteWithMiddleware }
Dan mesej ralat yang sedang saya terima:
BookContainer › should render without error TypeError: _BookAction.default.mockImplementation is not a function
Barisan ini dalam ujian unit BookContainer:
getBooksAction.mockImplementation(() => (dispatch) => {
Terima kasih atas sebarang bantuan atau nasihat. Saya telah mencari masalah dan penyelesaian yang serupa tetapi setakat ini tidak berjaya.
Jika saya menambah __esModule: true
pada olok-olok jenaka getBooksAction seperti ini:
jest.mock("../../../modules/book/BookAction", () => ({ __esModule: true, getBooksAction: jest.fn(), }));
Maka mesej ralat adalah berbeza:
TypeError: Cannot read properties of undefined (reading 'mockImplementation')
Jika saya menukar kunci getBooksAction kepada lalai dalam simulasi jenaka seperti ini:
jest.mock("../../../modules/book/BookAction", () => ({ __esModule: true, default: jest.fn(), }));
Maka tidak ada lagi ralat jenis, tetapi ralat penegasan (sedikit lebih dekat):
- 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
Jadi kini susunan buku kosong dikembalikan. Jadi bagaimana saya boleh menukar simulasi untuk menghantar pelbagai buku yang diberikan?
Saya rasa saya telah menemui punca masalah. Apabila BookContainer dibuat dan diberikan, buku diambil beberapa kali berturut-turut. Dua yang pertama mengembalikan tatasusunan buku kosong. Bermula dari kali ketiga, kembalikan susunan buku yang diperolehi. Saya tahu ini dengan menambahkan log konsol pada BookContainer selepas useEffect :
const booksResponse = useSelector(getBooksSelector); console.log(booksResponse);
Adakah ia perlu dipanggil berkali-kali berturut-turut? Bukankah ia sepatutnya hanya satu panggilan untuk mendapatkan susunan buku dengan betul? Apakah punca kelakuan ini, adakah terdapat sesuatu yang salah di tempat lain dalam kod saya?
Dengan cara ini, ini juga sebab saya mempunyai pernyataan IF yang menjengkelkan ini dalam komponen BookContainer. Walaupun tiada dalam tutorial, semuanya berfungsi seperti yang diharapkan. Permintaan/operasi nampaknya berganda setiap kali BookContainer dipaparkan...
Saya menggunakan StrictMode dalam fail indeks. Selepas mengalih keluarnya, permintaan berganda hilang dan useEffect() dalam BookContainer kini hanya dilaksanakan sekali. Tetapi kaedah pemaparan BookContainer masih dilaksanakan dua kali - kali pertama dengan tatasusunan buku kosong, dan kali kedua dengan tatasusunan buku yang diambil.
P粉9860280392024-02-27 00:23:34
Punca utama ialah pemetaan data tindak balas yang salah antara bahagian belakang dan bahagian hadapan saya.
Respons API saya untuk titik akhir dapatkan buku ialah ini:
{ "books": [...] }
Jadi pada asasnya ia bukan tatasusunan json tetapi objek json dengan tatasusunan di dalamnya. Seperti yang dikatakan oleh amalan tindak balas API yang baik, jadilah lebih fleksibel.
Walau bagaimanapun, pada bahagian hadapan saya, kod yang saya tulis pada dasarnya salah mengandaikan bahawa respons api hanyalah tatasusunan json dalam BookList:
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, };
Tukar kepada:
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, }) };
Kemudian sesuaikan perubahan ini dengan lebih lanjut dalam komponen BookList:
const BookList = ({booksResponse}) => { return ( <Box className={styles.bookList} ml={5}> {booksResponse.books.map((book) => { return ( <BookListItem book={book} key={book.id} /> ); })} </Box> ); }
Akhirnya juga dalam ujian unit:
expect(BookList).toHaveBeenLastCalledWith({ booksResponse: books }, {});
Dan olok-olok getBooksAction tidak memerlukan sebarang lalai atau __esModule:
jest.mock("../../../modules/book/BookAction", () => ({ getBooksAction: jest.fn(), }));
Semuanya berfungsi seperti yang diharapkan. :)