Je suis nouveau sur React et Jest et j'ai du mal avec presque tout jusqu'à présent. J'essaie de suivre un tutoriel que j'ai trouvé.
Il s'agit d'une simple application frontale React de librairie. Jusqu'à présent, j'ai créé un composant de mise en page simple, puis un composant BookList à l'intérieur du composant BookContainer qui contient la liste des livres récupérés. Ensuite, chaque livre possède un composant BookListItem.
Ensuite, j'ai un simple BookService et getAllBooks pour obtenir des livres de Rest Api dans le backend. De plus, j'ai un simple BookReducer, BookSelector et BookAction qui gèrent tous la sauvegarde et l'obtention à partir du magasin Redux.
J'utilise Redux, React-Hooks, Redux Toolkit, Jest et Javascript.
Lorsque je l'exécute dans un navigateur Web, tout fonctionne correctement, le livre est récupéré, enregistré dans le magasin, puis restitué dans le composant BookContainer.
Maintenant, j'essaie d'ajouter un test unitaire simple pour ce composant BookContainer et je cherche de l'aide.
Je souhaite que ce test unitaire vérifie si le composant BookList a été rendu (haveBeenCalledWith), c'est-à-dire la liste des livres que j'ai passés dans la méthode de rendu.
Je souhaite également me moquer d'une BookAction qui renvoie la liste des livres que je passe au rendu. C'est exactement ce avec quoi je suis aux prises en ce moment.
Voici mon composant 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;
Voici mon composant 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;
Voici mon 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;
Voici mon 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 }, {}); }); });
Voici le TestDataProvider de bookContainerStateWithData que j'utilise dans mes tests :
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 };
Voici la méthode d'assistance renderWithRedux() de TestSetupProvider que j'utilise dans mes tests :
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> ) });
Voici mon ReduxStoreHelper qui fournit createSoteWithMiddleware() utilisé dans 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 }
Et le message d'erreur que je reçois actuellement :
BookContainer › should render without error TypeError: _BookAction.default.mockImplementation is not a function
Cette ligne dans le test unitaire BookContainer :
getBooksAction.mockImplementation(() => (dispatch) => {
Merci pour toute aide ou conseil. J'ai recherché des problèmes et des solutions similaires, mais jusqu'à présent, sans succès.
Si j'ajoute __esModule: true
à la blague simulée de getBooksAction comme ceci :
jest.mock("../../../modules/book/BookAction", () => ({ __esModule: true, getBooksAction: jest.fn(), }));
Ensuite, le message d'erreur est différent :
TypeError: Cannot read properties of undefined (reading 'mockImplementation')
Si je change la clé getBooksAction par défaut dans une simulation de blague comme celle-ci :
jest.mock("../../../modules/book/BookAction", () => ({ __esModule: true, default: jest.fn(), }));
Il n'y a alors plus une erreur de type, mais une erreur d'assertion (un peu plus près) :
- 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
Alors maintenant, un tableau de livres vide est renvoyé. Alors, comment puis-je modifier la simulation pour distribuer la gamme de livres donnée ?
Je pense avoir trouvé la cause première du problème. Lorsqu'un BookContainer est créé et rendu, les livres sont récupérés plusieurs fois de suite. Les deux premiers renvoient des tableaux de livres vides. À partir de la troisième fois, renvoyez le tableau books obtenu. Je le sais en ajoutant le journal de la console au BookContainer après useEffect :
const booksResponse = useSelector(getBooksSelector); console.log(booksResponse);
Faut-il l'appeler plusieurs fois de suite ? Ne devrait-il pas suffire d'un simple appel pour obtenir correctement la gamme de livres ? Quelle est la cause de ce comportement ? Y a-t-il un problème ailleurs dans mon code ?
Au fait, c'est aussi pourquoi j'ai cette instruction IF ennuyeuse dans le composant BookContainer. Même si cela ne figure pas dans le didacticiel, tout fonctionne comme prévu. Les requêtes/opérations semblent doubler à chaque fois que le BookContainer est rendu...
J'ai utilisé StrictMode dans le fichier d'index. Après l'avoir supprimée, la double requête a disparu et useEffect() dans BookContainer n'est désormais exécuté qu'une seule fois. Mais la méthode de rendu de BookContainer est toujours exécutée deux fois : la première fois avec le tableau de livres vide et la deuxième fois avec le tableau de livres récupéré.
La cause première était un mauvais mappage des données de réponse entre mon backend et mon frontend.
Ma réponse API au point de terminaison get book est la suivante :
Donc, fondamentalement, ce n'est pas un tableau json mais un objet json avec un tableau à l'intérieur. Comme le disent les bonnes pratiques de réponse API, soyez plus flexible.
Cependant, sur mon frontend, le code que j'ai écrit suppose à tort que la réponse de l'API n'est qu'un tableau json dans une BookList :
Changez-le par :
Adaptez ensuite davantage ce changement dans le composant BookList :
Enfin aussi dans les tests unitaires :
Et la simulation getBooksAction ne nécessite aucune valeur par défaut ni __esModule :
Tout fonctionne comme prévu. :)