如何使用 Jest 在 React BooksContainer 单元测试中添加 GetBooksAction 的模拟?
P粉463291248
P粉463291248 2024-02-26 09:37:23
0
1
453

我是 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粉463291248
P粉463291248

全部回复(1)
P粉986028039

最终的根本原因是我的后端和前端之间的响应数据映射错误。

我对获取图书端点的 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(),
}));

一切都按预期进行。 :)

热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板