93

I have a simple Todo component that utilizes react-redux hooks that I'm testing using enzyme but I'm getting either an error or an empty object with a shallow render as noted below.

What is the correct way to test components using hooks from react-redux?

Todos.js

const Todos = () => {
  const { todos } = useSelector(state => state);

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
};

Todos.test.js v1

...

it('renders without crashing', () => {
  const wrapper = shallow(<Todos />);
  expect(wrapper).toMatchSnapshot();
});

it('should render a ul', () => {
  const wrapper = shallow(<Todos />);
  expect(wrapper.find('ul').length).toBe(1);
});

v1 Error:

...
Invariant Violation: could not find react-redux context value; 
please ensure the component is wrapped in a <Provider>
...

Todos.test.js v2

...
// imported Provider from react-redux 

it('renders without crashing', () => {
  const wrapper = shallow(
    <Provider store={store}>
      <Todos />
    </Provider>,
  );
  expect(wrapper).toMatchSnapshot();
});

it('should render a ul', () => {
  const wrapper = shallow(<Provider store={store}><Todos /></Provider>);
  expect(wrapper.find('ul').length).toBe(1);
});

v2 tests also fail since wrapper is the <Provider> and calling dive() on wrapper will return the same error as v1.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
crowns4days
  • 963
  • 1
  • 6
  • 6
  • Did you ever figure this out? I'm running into the same issue after migrating to Redux hooks. – Markus Hay Jul 04 '19 at 18:42
  • 2
    It appears to be an issue with Enzyme specifically, but so far there don't appear to be any adequate workarounds using the shallow renderer. Better hooks support should be in the next Enzyme release: https://github.com/airbnb/enzyme/issues/2011 – Markus Hay Jul 04 '19 at 18:50
  • Use mount instead of shallow, as shallow only renders the root component and places the custom child components as-is – Shivang Gupta Apr 16 '20 at 16:36

10 Answers10

78

To mock useSelector use can do this

import * as redux from 'react-redux'

const spy = jest.spyOn(redux, 'useSelector')
spy.mockReturnValue({ username:'test' })
Kriti
  • 2,163
  • 15
  • 16
53

I could test a component which uses redux hooks using enzyme mount facility and providing a mocked store to the Provider:

Component

import React from 'react';
import AppRouter from './Router'
import { useDispatch, useSelector } from 'react-redux'
import StartupActions from './Redux/Startup'
import Startup from './Components/Startup'
import './App.css';

// This is the main component, it includes the router which manages
// routing to different views.
// This is also the right place to declare components which should be
// displayed everywhere, i.e. sockets, services,...
function App () {
  const dispatch = useDispatch()
  const startupComplete = useSelector(state => state.startup.complete)

  if (!startupComplete) {
    setTimeout(() => dispatch(StartupActions.startup()), 1000)
  }

  return (
    <div className="app">
      {startupComplete ? <AppRouter /> : <Startup />}
    </div>
  );
}

export default App;

Test

import React from 'react';
import {Provider} from 'react-redux'
import { mount, shallow } from 'enzyme'
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk';
import App from '../App';

const mockStore = configureMockStore([thunk]);

describe('App', () => {
  it('should render a startup component if startup is not complete', () => {
    const store = mockStore({
      startup: { complete: false }
    });
    const wrapper = mount(
      <Provider store={store}>
        <App />
      </Provider>
    )
    expect(wrapper.find('Startup').length).toEqual(1)
  })
})
abidibo
  • 4,175
  • 2
  • 25
  • 33
  • 4
    It seems like Redux hooks tightly couple Redux to React components. And by wrapping your component in a Provider, wouldn't you always need to dive() through it? – png Jan 09 '20 at 23:59
  • 4
    I am surprised how come this is the accepted answer. This doesn't answer the question at all, using mount is not an alternative to shallow. – omeralper Oct 27 '20 at 20:19
  • 1
    @omeralper Since the question is "How to test a component using react-redux hooks?" I think yes, it answers the question. – abidibo Nov 25 '20 at 17:52
26

There is another way than @abidibo if you use a function selector defined in another file. You can mock useSelector and your selector function, and then use shallow from enzyme:

Component

import * as React from 'react';
import { useSelector } from 'react-redux';
import Spinner from './Spinner';
import Button from './Button ';
import { getIsSpinnerDisplayed } from './selectors';

const Example = () => {
  const isSpinnerDisplayed = useSelector(getIsSpinnerDisplayed);

  return isSpinnerDisplayed ? <Spinner /> : <Button />;
};

export default Example;

Selectors

export const getIsSpinnerDisplayed = state => state.isSpinnerDisplayed;

Test

import * as React from 'react';
import { shallow } from 'enzyme';
import Example from './Example';
import Button from './Button ';
import { getIsSpinnerDisplayed } from './selectors';

jest.mock('react-redux', () => ({
  useSelector: jest.fn(fn => fn()),
}));
jest.mock('./selectors');

describe('Example', () => {
  it('should render Button if getIsSpinnerDisplayed returns false', () => {
    getIsSpinnerDisplayed.mockReturnValue(false);

    const wrapper = shallow(<Example />);

    expect(wrapper.find(Button).exists()).toBe(true);
  });
});

It may be a little bit hacky, but it works well for me :)

jhujhul
  • 1,527
  • 12
  • 11
24

Testing React Redux Hooks With Enzyme's Shallow Rendering

After reading through all the responses here and digging through the documentation, I wanted to aggregate the ways to test React components using react-redux hooks with Enzyme and shallow rendering.

These tests rely on mocking the useSelector and useDispatch hooks. I'll also provide examples in both Jest and Sinon.

Basic Jest Example

import React from 'react';
import { shallow } from 'enzyme';
import * as redux from 'react-redux';
import TodoList from './TodoList';

describe('TodoList', () => {
  let spyOnUseSelector;
  let spyOnUseDispatch;
  let mockDispatch;

  beforeEach(() => {
    // Mock useSelector hook
    spyOnUseSelector = jest.spyOn(redux, 'useSelector');
    spyOnUseSelector.mockReturnValue([{ id: 1, text: 'Old Item' }]);

    // Mock useDispatch hook
    spyOnUseDispatch = jest.spyOn(redux, 'useDispatch');
    // Mock dispatch function returned from useDispatch
    mockDispatch = jest.fn();
    spyOnUseDispatch.mockReturnValue(mockDispatch);
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  it('should render', () => {
    const wrapper = shallow(<TodoList />);

    expect(wrapper.exists()).toBe(true);
  });

  it('should add a new todo item', () => {
    const wrapper = shallow(<TodoList />);

    // Logic to dispatch 'todoAdded' action

    expect(mockDispatch.mock.calls[0][0]).toEqual({
      type: 'todoAdded',
      payload: 'New Item'
    });
  });
});

Basic Sinon Example

import React from 'react';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import * as redux from 'react-redux';
import TodoList from './TodoList';

describe('TodoList', () => {
  let useSelectorStub;
  let useDispatchStub;
  let dispatchSpy;  

  beforeEach(() => {
    // Mock useSelector hook
    useSelectorStub = sinon.stub(redux, 'useSelector');
    useSelectorStub.returns([{ id: 1, text: 'Old Item' }]);

    // Mock useDispatch hook
    useDispatchStub = sinon.stub(redux, 'useDispatch');
    // Mock dispatch function returned from useDispatch
    dispatchSpy = sinon.spy();
    useDispatchStub.returns(dispatchSpy);
  });

  afterEach(() => {
    sinon.restore();
  });
  
  // More testing logic...
});

Testing Multiple useSelector Hooks

Testing multiple useSelectors requires us to mock the Redux app state.

var mockState = {
  todos: [{ id: 1, text: 'Old Item' }]
};

Then we can mock our own implementation of useSelector.

// Jest
const spyOnUseSelector = jest.spyOn(redux, 'useSelector').mockImplementation(cb => cb(mockState));

// Sinon
const useSelectorStub = sinon.stub(redux, 'useSelector').callsFake(cb => cb(mockState));
Andrew W
  • 241
  • 2
  • 4
  • This should be the accepted answers as it covers cleanly how to test effectively the needed hooks. – Mel Macaluso Jun 27 '21 at 03:25
  • was looking for a simpler way to mock multiple useSelectors instead of writing it using `.mockImplementationOnce` n number of times. thankyou @Andrew W – Maulik Sheth Aug 05 '21 at 18:21
8

I think this is both the best and the simplest way to mock useSelector hook from Redux store in jest:

  import * as redux from 'react-redux'

  const user = {
    id: 1,
    name: 'User',
  }

  const state = { user }

  jest
    .spyOn(redux, 'useSelector')
    .mockImplementation((callback) => callback(state))

With the idea being that you are able to provide the state store mock with just a subset of store data.

Paweł Gościcki
  • 9,066
  • 5
  • 70
  • 81
3

After searching for help I combined some of the methods I found to mock useSelector.

First create a function that does some bootstrapping before your test. Setting up the store with some values that you want to overwrite and mock the useSelector function of react-redux.

I think it is really useful for creating multiple testcases where u see how the store state influences the behaviour of your component.

import configureMockStore from 'redux-mock-store';
import * as Redux from 'react-redux';
import MyComponent from './MyComponent';

const mockSelectors = (storeValues) => {
  const mockStore = configureMockStore()({
    mobile: {
      isUserConnected: false
      ...storeValues
    },
  });

  jest
    .spyOn(Redux, 'useSelector')
    .mockImplementation(state => state.dependencies[0](mockStore.getState()));
};

describe('isUserConnected: true', () => {
    beforeEach(() => {
      mockSelectors({ isUserConnected: true });
      component = shallow(<MyComponent />);

      test('should render a disconnect button', () => {
         expect(component).toBeDefined();
         expect(component.find('button')).toBeTruthy();
      });
    });
  });

And the component:

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

const MyComponent = () => {    
  const isConnected = useSelector(selectIsConnected, shallowEqual);

  return (
    <>
      {
        showDisconnect ? (
          <button type="button" onClick={() => ()}>disconnect</button>
        ) : null
      }
    </>
  );
};

export default MyComponent;
2

Below code works for me.

import { configure, shallow} from 'enzyme'; 
import Adapter from 'enzyme-adapter-react-16'; 
import ServiceListingScreen  from './ServiceListingScreen'; 
import renderer from 'react-test-renderer';
import { createStore } from 'redux';
import serviceReducer from './../store/reducers/services'; 
import { Provider } from 'react-redux'
 
const store = createStore(serviceReducer  ) ; 
configure({adapter: new Adapter()}); 


const ReduxProvider = ({ children, reduxStore }) => (
  <Provider store={reduxStore}>{children}</Provider>
)

describe('Screen/ServiceListingScreen', () => {
  it('renders correctly ', () => {
    const wrapper = shallow(<Provider store={store}><ServiceListingScreen  /></Provider>);
    const tree = renderer.create(wrapper).toJSON();
    expect(tree).toMatchSnapshot();
  });
   
});

0

You can try redux-saga-test-plan cool redux assertion and running library and lightweight it doest all your heavy lifting of running saga and assertion automatically

const mockState = { rick:"Genius", morty:"dumbAsss"}

expectSaga(yourCoolSaga)
      .provide({
        select({ selector }, next) {
          if (selector) {
            return mockState;
          }
          return next();
        }
      })
    // some assertion here
      .put(actions.updateRickMortyPowers(mockState))
      .silentRun();
Ashif Zafar
  • 623
  • 5
  • 6
0

Some of the answers above mocked the useSelector or useDispatch but the redux docs actually advice against that...check out this answer here: https://stackoverflow.com/a/74024854/14432913 which worked for me and followed the example in redux docs

goodnews john
  • 431
  • 4
  • 9
-4

this is worked for me as well :

import { shallow, mount } from "enzyme";
const store = mockStore({
  startup: { complete: false }
});

describe("==== Testing App ======", () => {
  const setUpFn = props => {
    return mount(
      <Provider store={store}>
        <App />
      </Provider>
    );
  };

  let wrapper;
  beforeEach(() => {
    wrapper = setUpFn();
  });
Hilal Aissani
  • 709
  • 8
  • 3