搜尋

首頁  >  問答  >  主體

如何使用 Jest 在 React BooksContainer 單元測試中新增 GetBooksAction 的模擬?

我是 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) => {

感謝您的任何幫助或建議。我一直在尋找類似的問題和解決方案,但到目前為止還沒有成功。

編輯 1

如果我將 __esModule: true 加入到 getBooksAction 的笑話模擬中,如下所示:

jest.mock("../../../modules/book/BookAction", () => ({
    __esModule: true,
    getBooksAction: jest.fn(),
}));

那麼錯誤訊息就不同了:

TypeError: Cannot read properties of undefined (reading 'mockImplementation')

編輯2

如果我在玩笑模擬中將 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

所以現在回傳了空的書籍陣列。那麼如何更改模擬來分派給定的書籍陣列呢?

編輯3

我想我已經找到問題的根本原因了。創建和渲染 BookContainer 時,會連續多次取得書籍。前兩個返回空的書籍數組。從第三次開始,返回所獲得的 books 陣列。我透過在 useEffect 之後將控制台日誌新增至 BookContainer 來知道這一點:

const booksResponse = useSelector(getBooksSelector);
console.log(booksResponse);

它應該連續被呼叫很多次嗎?難道不應該只是一次正確取得書籍數組的調用嗎?造成這種行為的原因是什麼,是否是我的程式碼在其他地方出現了錯誤?

順便說一句,這也是我在 BookContainer 元件中出現這個令人討厭的 IF 語句的原因。儘管在教程中沒有,但一切都按預期工作。每次渲染 BookContainer 時,請求/操作似乎都會加倍...

編輯4

我在索引檔中使用了 StrictMode。刪除它後,雙倍的請求消失了,BookContainer 中的 useEffect() 現在只執行一次。但 BookContainer 的 render 方法仍然執行兩次 - 第一次使用空書籍數組,第二次使用獲取的書籍數組。

P粉463291248P粉463291248261 天前447

全部回覆(1)我來回復

  • P粉986028039

    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(),
    }));

    一切都如預期進行。 :)

    回覆
    0
  • 取消回覆