1
votes

I've got a simple component that uses the useSelector and useDispatch hooks to dispatch and get the current state from my redux store and map over them to return a child component.

I'm new to testing with hooks and after following some articles, I mocked the two redux hooks with jest mock to see if they are called when mounted, however, when running the test my state array is returned as undefined so the test fails. Does anyone know why this is happening?

COMPONENT.tsx

import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { getData } from '../actions/index';
import { AppState } from '../store/store';

import GridItem from '../component/GridItem';

function GridContainer () {        
    const dispatch = useDispatch();
    const books = useSelector((state: AppState) => state);

    useEffect(() => {
        dispatch(getData());
    }, [dispatch]);
   
    const bookList = (list: AppState) => {
        return list.map((book) => (
            <GridItem key={book.id} data={ book } />
        ))
    };

    return (
        <div className="book-container">
            { bookList(books) }
        </div>
    )
}

export default GridContainer;

COMPONENT.test.tsx

import React from 'react';
import { shallow, mount } from 'enzyme';

import GridContainer from './GridContainer';

const mockDispatch = jest.fn();

jest.mock("react-redux", () => ({
    useSelector: jest.fn(fn => fn()),
    useDispatch: () => mockDispatch
}));

it('renders container', () => {
    const wrapper = shallow(<GridContainer />);
    const component = wrapper.find('.book-container');

    expect(component.length).toBe(1);
});

describe('react hooks', () => {
    afterEach(() => {
        jest.clearAllMocks();
    });

    it('runs dispatch on mount', () => {
        const wrapper = mount(<GridContainer />);

        expect(mockDispatch).toHaveBeenCalledTimes(1);
    });
});

ERROR msg

 TypeError: Cannot read property 'map' of undefined

      25 |     //function to print list of books component
      26 |     const bookList = (list: AppState) => {
    > 27 |         return list.map((book) => (
         |                     ^
      28 |             <GridItem key={book.id} data={ book } />
      29 |         ))
      30 |     };
2

2 Answers

0
votes

the problem is with mocking useSelector. the function itself pass AppState to a callback, but a mock function is not aware of AppState. In this way your mock will return undefined. In this way books (list) will be undefined.

you need to specify what your mock will return, in this case an array. you can call mockReturnValue passing a booksMock (an array) for that:

jest.mock("react-redux", () => ({
    useSelector: jest.fn().mockReturnValue(booksMock),
    useDispatch: () => mockDispatch
}));
0
votes

You should mock the return value of useSelector hook.

E.g.

GridContainer.tsx:

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { getData } from './actions/index';
import { AppState } from './store/store';
import GridItem from './component/GridItem';

function GridContainer() {
  const dispatch = useDispatch();
  const books = useSelector((state: AppState) => state);
  console.log('books:', books);

  useEffect(() => {
    dispatch(getData());
  }, [dispatch]);

  const bookList = (list: AppState) => {
    return list.map((book) => <GridItem key={book.id} data={book} />);
  };

  return <div className="book-container">{bookList(books)}</div>;
}

export default GridContainer;

./component/GridItem.tsx:

import React from 'react';

export default function GridItem({ data }) {
  return <div>{data.name}</div>;
}

./store/store.ts:

export type AppState = any[];

./actions/index.ts:

export function getData() {
  return { type: 'GET_DATA' };
}

GridContainer.test.tsx:

import React from 'react';
import { shallow, mount } from 'enzyme';
import GridContainer from './GridContainer';
import { useDispatch, useSelector } from 'react-redux';

const mockDispatch = jest.fn();
const booksMock = [
  { id: 1, name: 'golang' },
  { id: 2, name: 'TypeScript' },
];

jest.mock('react-redux', () => {
  return {
    useSelector: jest.fn(() => booksMock),
    useDispatch: jest.fn(() => mockDispatch),
  };
});

describe('react hooks', () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  it('renders container', () => {
    const wrapper = shallow(<GridContainer />);
    const component = wrapper.find('.book-container');
    expect(component.length).toBe(1);
  });

  it('runs dispatch on mount', () => {
    const wrapper = mount(<GridContainer />);
    expect(useDispatch).toBeCalledTimes(1);
    expect(useSelector).toBeCalledWith(expect.any(Function));
    expect(mockDispatch).toBeCalledWith({ type: 'GET_DATA' });
    expect(wrapper.find('.book-container').children()).toHaveLength(2);
  });
});

unit test result:

 PASS  examples/65172098/GridContainer.test.tsx
  react hooks
    ✓ renders container (65 ms)
    ✓ runs dispatch on mount (44 ms)

  console.log
    books: [ { id: 1, name: 'golang' }, { id: 2, name: 'TypeScript' } ]

      at GridContainer (examples/65172098/GridContainer.tsx:11:11)

  console.log
    books: [ { id: 1, name: 'golang' }, { id: 2, name: 'TypeScript' } ]

      at GridContainer (examples/65172098/GridContainer.tsx:11:11)

--------------------|---------|----------|---------|---------|-------------------
File                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------------|---------|----------|---------|---------|-------------------
All files           |      95 |      100 |   85.71 |     100 |                   
 65172098           |   93.33 |      100 |      80 |     100 |                   
  GridContainer.tsx |   93.33 |      100 |      80 |     100 |                   
 65172098/actions   |     100 |      100 |     100 |     100 |                   
  index.ts          |     100 |      100 |     100 |     100 |                   
 65172098/component |     100 |      100 |     100 |     100 |                   
  GridItem.tsx      |     100 |      100 |     100 |     100 |                   
--------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        5.442 s

source code: https://github.com/mrdulin/jest-v26-codelab/tree/main/examples/65172098