如何在React BooksContainer单元测试中使用Jest为GetBooksAction添加模拟?

huangapple go评论56阅读模式
英文:

How to add mock for GetBooksAction in React BooksContainer unit test using Jest?

问题

Here is the translated portion of your content:

我对React和Jest都很陌生,到目前为止几乎所有事情都很困难。我正在尝试跟随我找到的教程。

这是一个简单的React前端应用程序,用于书店。到目前为止,我已经创建了一个简单的布局组件,然后在BookContainer组件内部,其中包含一个具有获取的书籍列表的BookList组件。然后每本书都有一个单独的BookListItem组件。

然后我有一个简单的BookService,其中包含一个用于从后端Rest Api获取书籍的getAllBooks方法。此外,我还有一个简单的BookReducer、BookSelector和BookAction,它们都用于保存和从Redux存储中获取数据。

我使用了redux、react-hooks、redux toolkit、jest和javascript。

当我在web浏览器中运行它时,一切都正常工作,书籍被获取、保存到存储中,然后在BookContainer组件中呈现。

现在我正在尝试为这个BookContainer组件添加一个简单的单元测试,我需要帮助。

我希望这个单元测试能够检查BookList组件是否已呈现(haveBeenCalledWith),以及传递给渲染方法的书籍列表。

而且,我想为此模拟BookAction,以返回我传递给渲染方法的书籍列表。这正是我目前遇到困难的地方。

这是我的BookContainer组件...

Please note that due to the length and complexity of your content, I provided a portion of the translation. If you need the translation for the remaining content or have specific questions about it, please let me know.

英文:

I am new to React and Jest and am struggling with almost everything so far. I am trying to follow along with tutorial I have found.

This is simple React frontend application for book store. So far I have created a simple layout component, then inside BookContainer component, in which BookList component with list of books fetched is presented. Then each book has a single BookListItem component.

Then I have simple BookService with getAllBooks for fetching books from a Rest Api on backend side. Additionally I also have a simple BookReducer, BookSelector and BookAction, which all handle saving to and fetching from a Redux store.

I am using redux, react-hooks, redux toolkit, jest and javascript.

Everything works ok when I run it in a web browser, the books are fetched, saved into store, and then presented inside the BookContainer component.

Now I am trying to add a simple unit test for this BookContainer component and I am asking for help with it.

I would like this unit test to check if the BookList component is rendered (haveBeenCalledWith), the list of books which I pass into the render method.

And also I would like to mock for this the BookAction, to return the list of books which I am passing into render. And this is exactly what I struggle with right now.

Here is my BookContainer component:

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;

Here is my BookList component:

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;

Here is my 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;

Here is my 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 }, {});

});

});

Here is my TestDataProvider for bookContainerStateWithData used in test:

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
};

And here is my renderWithRedux() helper method from TestSetupProvider used in test:

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

Here is my ReduxStoreHelper which provides createSoteWithMiddleware() used in 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
}

And the error message which I get currently:

BookContainer › should render without error

TypeError: _BookAction.default.mockImplementation is not a function

On this line in BookContainer unit test:

getBooksAction.mockImplementation(() => (dispatch) => {

Thank you for any help or suggestions. I was searching for similar problems and solutions, but so far not successfully.

Edit 1

If I add __esModule: true to the jest mock for getBooksAction, like so:

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

Then the error message is different:

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

Edit2

If I change getBooksAction key to default in jest mock like so:

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

Then there is no type error anymore, but assertion error instead (closer a bit):

- 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

So empty array of books is returned now. So how to change the mock to dispatch the given array of books ?

Edit 3

I guess I have found the root cause of the problem. When the BookContainer is being created and rendered, the fetching of books happens several times in a row. The first two return empty array of books. And starting from the third time, the fetched books array is returned. I know it by adding console log to BookContainer, just after useEffect:

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

Is it supposed to be called that many times in a row ? Shouldnt it be just one call with properly fetched books array. What can be the cause of this behavior, is it some error in my code somewhere else ?

By the way this is also the reason why I have this nasty IF statement in my BookContainer component. Although in the tutorial there is not and it all works as expected. It seems like the request / actions are doubled every time BookContainer is being rendered...

Edit 4

I was using StrictMode in my index file. After removing it, the doubled requests are gone, the useEffect() in BookContainer executes only once now. But still the render method of BookContainer executes twice - first time with empty books array, and second time with fetched books array.

答案1

得分: 0

以下是您要翻译的内容:

"It ended up to be wrong mapping of response data between my backend and frontend as a root cause of it.

My api response for Get Books endpoint is this:

{
"books": [...]
}

So basicly it is not a json array, but json object with array inside it. As a good practices for api responses say, to be more flexibile.

But, on my frontend side, the code which I've written, basicly wrongly assumed that api response is just a json array, in 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,

};

When changing it to this:

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

And then further in BookList component, adapt to this change:

const BookList = ({booksResponse}) => {

return (
<Box className={styles.bookList} ml={5}>
{booksResponse.books.map((book) => {
return (
<BookListItem book={book} key={book.id} />
);
})}
</Box>
);
}

And then finally in the unit test also:

expect(BookList).toHaveBeenLastCalledWith({ booksResponse: books }, {});

And getBooksAction mock no need for any default or __esModule:

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

Everything works as expected. :)"

请注意,上述内容中包含了HTML转义字符和编程符号,已经按照您的要求将其保留在翻译中。

英文:

It ended up to be wrong mapping of response data between my backend and frontend as a root cause of it.

My api response for Get Books endpoint is this:

{
    &quot;books&quot;: [...]
}

So basicly it is not a json array, but json object with array inside it. As a good practices for api responses say, to be more flexibile.

But, on my frontend side, the code which I've written, basicly wrongly assumed that api response is just a json array, in 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,
};

When changing it to this:

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

And then further in BookList component, adapt to this change:

const BookList = ({booksResponse}) =&gt; {

  return (
    &lt;Box className={styles.bookList} ml={5}&gt;
        {booksResponse.books.map((book) =&gt; {
            return (
                &lt;BookListItem book={book} key={book.id} /&gt;
            );
        })}
    &lt;/Box&gt;
  );
}

And then finally in the unit test also:

expect(BookList).toHaveBeenLastCalledWith({ booksResponse: books }, {});

And getBooksAction mock no need for any default or __esModule:

jest.mock(&quot;../../../modules/book/BookAction&quot;, () =&gt; ({
    getBooksAction: jest.fn(),
}));

Everything works as expected. 如何在React BooksContainer单元测试中使用Jest为GetBooksAction添加模拟?

huangapple
  • 本文由 发表于 2023年6月25日 18:05:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/76549855.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定