I'm having some troubles testing that a prop is fired on my HOC.
import { connect } from 'react-redux';
import { compose, lifecycle } from 'recompose';
import { fetchCurrentUser } from '../../actions/users';
import { getUser } from '../../reducers/users';
import User from '../../models/User';
export default Component => compose(
connect(state => ({
user: getUser(state),
}),
{ fetchCurrentUser }),
lifecycle({
componentDidMount() {
if (this.props.user instanceof User) return;
this.props.fetchCurrentUser();
},
}),
)(Component);
What I'd like to know is whether or not fetchCurrentUser is trigger when user is NOT a User instance.
So far I have that in my test:
it.only('fetches user if user is not a User instance', () => {
const setup = () => {
const props = {
user: 'string',
fetchCurrentUser: jest.fn(),
};
const enzymeWrapper = mounting(props);
return {
props,
enzymeWrapper,
};
};
// That returns 0 so false
expect(setup().props.fetchCurrentUser.mock.calls.length).toEqual(1);
});
It seems like I can't replace the props doing it this way. If I log this.props in the lifecycle method, I never see user: 'string'
Thanks in advance
You would need to shallow mount the component in order to test its functionality.
it.only('fetches user if user is not a User instance', () => {
const setup = () => {
const props = {
user: 'string',
fetchCurrentUser: jest.fn(),
};
// shallow render the component
const enzymeWrapper = shallow(<Component {...props} />)
return {
props,
enzymeWrapper,
};
};
expect(setup().props.fetchCurrentUser.mock.calls.length).toEqual(1);
});
OK so, with shubham-khatri's help, here's what I did to make it work.
Separated the component into 2 different ones, and tested the one with the call only. That way I could mock the passed props from the test.
Component:
import { connect } from 'react-redux';
import { compose, lifecycle } from 'recompose';
import { fetchCurrentUser } from '../../actions/users';
import { getUser } from '../../reducers/users';
import User from '../../models/User';
const Connected = connect(state => ({
user: getUser(state),
}),
{ fetchCurrentUser });
export const Enhanced = lifecycle({
componentDidMount() {
if (this.props.user instanceof User) return;
this.props.fetchCurrentUser();
},
});
export default Component => compose(
Connected,
Enhanced,
)(Component);
Test:
describe('Fetching user', () => {
const setup = (moreProps) => {
const props = {
fetchCurrentUser: jest.fn(),
...moreProps,
};
const EnhancedStub = compose(
Enhanced,
)(Component);
const enzymeWrapper = shallow(
<EnhancedStub {...props} />,
);
return {
props,
enzymeWrapper,
};
};
it('fetches user if user is not a User instance', () => {
expect(setup().props.fetchCurrentUser.mock.calls.length).toEqual(1);
});
it('does NOT fetch user if user is a User instance', () => {
expect(setup({ user: new User({ first_name: 'Walter' }) }).props.fetchCurrentUser.mock.calls.length).toEqual(0);
});
});
Hope that helps someone.
Related
hope someone can point me the right direction with this. Basically I've created a react app which makes use of hooks, specifically useContext, useEffect and useReducer. My problem is that I can't seem to get tests to detect click or dispatch events of the related component.
Stripped down version of my app can be found at : https://github.com/atlantisstorm/hooks-testing
Tests relate to layout.test.js script.
I've tried various approaches, different ways of mocking dispatch, useContext, etc but no joy with it. Most recent version.
layout.test.js
import React from 'react';
import { render, fireEvent } from "#testing-library/react";
import Layout from './layout';
import App from './app';
import { Provider, initialState } from './context';
const dispatch = jest.fn();
const spy = jest
.spyOn(React, 'useContext')
.mockImplementation(() => ({
state: initialState,
dispatch: dispatch
}));
describe('Layout component', () => {
it('starts with a count of 0', () => {
const { getByTestId } = render(
<App>
<Provider>
<Layout />
</Provider>
</App>
);
expect(dispatch).toHaveBeenCalledTimes(1);
const refreshButton = getByTestId('fetch-button');
fireEvent.click(refreshButton);
expect(dispatch).toHaveBeenCalledTimes(3);
});
});
layout.jsx
import React, { useContext, useEffect } from 'react';
import { Context } from "./context";
const Layout = () => {
const { state, dispatch } = useContext(Context);
const { displayThings, things } = state;
const onClickDisplay = (event) => {
// eslint-disable-next-line
event.preventDefault;
dispatch({ type: "DISPLAY_THINGS" });
};
useEffect(() => {
dispatch({ type: "FETCH_THINGS" });
}, [displayThings]);
const btnText = displayThings ? "hide things" : "display things";
return (
<div>
<button data-testid="fetch-button" onClick={onClickDisplay}>{btnText}</button>
{ displayThings ?
<p>We got some things!</p>
:
<p>No things to show!</p>
}
{ displayThings && things.map((thing) =>
<p>{ thing }</p>
)}
</div>
)
}
export default Layout;
app.jsx
import React from 'react';
import Provider from "./context";
import Layout from './layout';
const App = () => {
return (
<Provider>
<Layout />
</Provider>
)
}
export default App;
context.jsx
import React, { createContext, useReducer } from "react";
import { reducer } from "./reducer";
export const Context = createContext();
export const initialState = {
displayThings: false,
things: []
};
export const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
);
};
export default Provider;
reducer.jsx
export const reducer = (state, action) => {
switch (action.type) {
case "DISPLAY_THINGS": {
const displayThings = state.displayThings ? false : true;
return { ...state, displayThings };
}
case "FETCH_THINGS": {
const things = state.displayThings ? [
"thing one",
"thing two"
] : [];
return { ...state, things };
}
default: {
return state;
}
}
};
I'm sure the answer will be easy when I see it, but just trying to figure out I can detect the click event plus detect the 'dispatch' events? (I've already got separate test in the main app to properly test dispatch response/actions)
Thank you in advance.
EDIT
Ok, I think I've got a reasonable, though not perfect, solution. First I just added optional testDispatch and testState props to the context.jsx module.
new context.jsx
import React, { createContext, useReducer } from "react";
import { reducer } from "./reducer";
export const Context = createContext();
export const initialState = {
displayThings: false,
things: []
};
export const Provider = ({ children, testDispatch, testState }) => {
const [iState, iDispatch] = useReducer(reducer, initialState);
const dispatch = testDispatch ? testDispatch : iDispatch;
const state = testState ? testState : iState;
return (
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
);
};
export default Provider;
Then in layout.test.jsx I just simply pass in mocked jest dispatch function plus state as necessary. Also removed the outer App wrapping as that seemed to prevent the props from being passed through.
new layout.test.jsx
import React from 'react';
import { render, fireEvent } from "#testing-library/react";
import Layout from './layout';
import { Provider } from './context';
describe('Layout component', () => {
it('starts with a count of 0', () => {
const dispatch = jest.fn();
const state = {
displayThings: false,
things: []
};
const { getByTestId } = render(
<Provider testDispatch={dispatch} testState={state}>
<Layout />
</Provider>
);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: "FETCH_THINGS" });
const refreshButton = getByTestId('fetch-things-button');
fireEvent.click(refreshButton);
expect(dispatch).toHaveBeenCalledTimes(2);
// Important: The order of the calls should be this, but dispatch is reporting them
// the opposite way around in the this test, i.e. FETCH_THINGS, then DISPLAY_THINGS...
//expect(dispatch).toHaveBeenNthCalledWith(1, { type: "DISPLAY_THINGS" });
//expect(dispatch).toHaveBeenNthCalledWith(2, { type: "FETCH_THINGS" });
// ... so as dispatch behaves correctly outside of testing for the moment I'm just settling for
// knowing that dispatch was at least called twice with the correct parameters.
expect(dispatch).toHaveBeenCalledWith({ type: "DISPLAY_THINGS" });
expect(dispatch).toHaveBeenCalledWith({ type: "FETCH_THINGS" });
});
});
One little caveat though, as noted above, when the 'fetch-things-button' was fired, it reported the dispatch in the wrong order. :/ So I just settled for knowing the correct calls where triggered, but if anyone knows why the call order isn't as expected I would be pleased to know.
https://github.com/atlantisstorm/hooks-testing update to reflect the above if anyone is interested.
A few months back I was also trying to write unit tests for reducer + context for an app. So, here's my solution to test useReducer and useContext.
FeaturesProvider.js
import React, { createContext, useContext, useReducer } from 'react';
import { featuresInitialState, featuresReducer } from '../reducers/featuresReducer';
export const FeatureContext = createContext();
const FeaturesProvider = ({ children }) => {
const [state, dispatch] = useReducer(featuresReducer, featuresInitialState);
return <FeatureContext.Provider value={{ state, dispatch }}>{children}</FeatureContext.Provider>;
};
export const useFeature = () => useContext(FeatureContext);
export default FeaturesProvider;
FeaturesProvider.test.js
import React from 'react';
import { render } from '#testing-library/react';
import { renderHook } from '#testing-library/react-hooks';
import FeaturesProvider, { useFeature, FeatureContext } from './FeaturesProvider';
const state = { features: [] };
const dispatch = jest.fn();
const wrapper = ({ children }) => (
<FeatureContext.Provider value={{ state, dispatch }}>
{children}
</FeatureContext.Provider>
);
const mockUseContext = jest.fn().mockImplementation(() => ({ state, dispatch }));
React.useContext = mockUseContext;
describe('useFeature test', () => {
test('should return present feature toggles with its state and dispatch function', () => {
render(<FeaturesProvider />);
const { result } = renderHook(() => useFeature(), { wrapper });
expect(result.current.state.features.length).toBe(0);
expect(result.current).toEqual({ state, dispatch });
});
});
featuresReducer.js
import ApplicationConfig from '../config/app-config';
import actions from './actions';
export const featuresInitialState = {
features: [],
environments: ApplicationConfig.ENVIRONMENTS,
toastMessage: null
};
const { INITIALIZE_DATA, TOGGLE_FEATURE, ENABLE_OR_DISABLE_TOAST } = actions;
export const featuresReducer = (state, { type, payload }) => {
switch (type) {
case INITIALIZE_DATA:
return {
...state,
[payload.name]: payload.data
};
case TOGGLE_FEATURE:
return {
...state,
features: state.features.map((feature) => (feature.featureToggleName === payload.featureToggleName
? {
...feature,
environmentState:
{ ...feature.environmentState, [payload.environment]: !feature.environmentState[payload.environment] }
}
: feature))
};
case ENABLE_OR_DISABLE_TOAST:
return { ...state, toastMessage: payload.message };
default:
return { ...state };
}
};
featuresReducer.test.js
import { featuresReducer } from './featuresReducer';
import actions from './actions';
const { INITIALIZE_DATA, TOGGLE_FEATURE, ENABLE_OR_DISABLE_TOAST } = actions;
describe('Reducer function test', () => {
test('should initialize data when INITIALIZE_DATA action is dispatched', () => {
const featuresState = {
features: []
};
const action = {
type: INITIALIZE_DATA,
payload: {
name: 'features',
data: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
}
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
});
});
test('should toggle the feature for the given feature and environemt when TOGGLE_FEATURE action is disptched', () => {
const featuresState = {
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}, {
featureId: '23458', featureName: 'WO photo download', featureToggleName: '23458_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
};
const action = {
type: TOGGLE_FEATURE,
payload: { featureToggleName: '23456_WOPhotoDownload', environment: 'sit' }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}, {
featureId: '23458', featureName: 'WO photo download', featureToggleName: '23458_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
});
});
test('should enable the toast message when ENABLE_OR_DISABLE_TOAST action is dispatched with the message as part of payload', () => {
const featuresState = {
toastMessage: null
};
const action = {
type: ENABLE_OR_DISABLE_TOAST,
payload: { message: 'Something went wrong!' }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({ toastMessage: 'Something went wrong!' });
});
test('should disable the toast message when ENABLE_OR_DISABLE_TOAST action is dispatched with message as null as part of payload', () => {
const featuresState = {
toastMessage: null
};
const action = {
type: ENABLE_OR_DISABLE_TOAST,
payload: { message: null }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({ toastMessage: null });
});
test('should return the current state when the action with no specific type is dispatched', () => {
const featuresState = {
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}]
};
const action = {};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}]
});
});
});
I am new to React with Redux and I am trying to create a TestUnit for a container which connects to a dialog that renders a table.
In the container I have:
a mapStateToProps constant which returns the properties for the
dialog with the tabel.
a mapDispatchToProps constant which maps a function to a action
a connect function which binds the 2 props from above to the Dialog.
The container:
import { ApplicationState } from '../ApplicationState';
import { connect } from 'react-redux';
import TabelDialog, { TabelDialogProps } from '../components/dialogs/TabelDialog';
import { acceptDisplay } from '../actions/displayActions';
const mapStateToProps=(
state: ApplicationState,
{ open, onClose }: TabelDialogProps)
=> {
return {
open,
onClose,
tableData: [
{
manufacturer: 'Samsung',
displayType: 'OLED',
},
{
manufacturer: 'LG',
displayType: 'OLED',
}
]
}
};
const mapDispatchToProps = {
onAccept: acceptDisplay
};
export default connect(mapStateToProps, mapDispatchToProps)(TabelDialog);
The test is not complete and requires adjustments:
const createMockStore = () => {
const store = {
getState: jest.fn(() => ({})),
dispatch: jest.fn(),
data: jest.fn()
}
const next = jest.fn()
const invoke = action => thunk(store)(next)(action)
return {store, next, invoke }
}
describe('Container test', () => {
it('should dispatch open', () => {
const store = createMockStore();
const wrapper = shallow(
<TabelDialog
open = {true}
data = {undefined}
onClose = {undefined}
onAccept = {undefined}
/>
);
expect(store).toHaveBeenCalled():
});
})
I would like to dipatch open, onClose and data. Am I on the right path?
Andy
Not sure if this will be the solution to your problem but maybe give you some progress.
It seems like you also have to provide the store to the component.
const store = {...}
const wrapper = shallow(<TabelDialog store={store} ... />);
Hi I am writing test for functional component using the jest and enzyme. and When I simulate a click then params(state of component using useState) of component change. and when state is changed then useEffect call and in useEffect I am dispatching some asynchronous actions with params after changed. So I want to test params with I am dispatching the action. for this I want to mock dispatch. How can I achieve this ?
Anyone can help me, thanks in advance. Below I am sharing the code.
component.js
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useSelector, useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { clientOperations, clientSelectors } from '../../store/clients';
import Breadcrumb from '../../components/UI/Breadcrumb/Breadcrumb.component';
import DataTable from '../../components/UI/DataTable/DataTable.component';
import Toolbar from './Toolbar/Toolbar.component';
const initialState = {
search: '',
type: '',
pageNo: 0,
rowsPerPage: 10,
order: 'desc',
orderBy: '',
paginated: true,
};
const Clients = ({ history }) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const totalElements = useSelector(state => state.clients.list.totalElements);
const records = useSelector(clientSelectors.getCompaniesData);
const [params, setParams] = useState(initialState);
useEffect(() => {
dispatch(clientOperations.fetchList(params));
}, [dispatch, params]);
function updateParams(newParams) {
setParams(state => ({
...state,
...newParams,
}));
}
function searchHandler(value) {
updateParams({
search: value,
pageNo: 0,
});
}
function typeHandler(event) {
updateParams({
type: event.target.value,
pageNo: 0,
});
}
function reloadData() {
setParams(initialState);
}
const columns = {
id: t('CLIENTS_HEADING_ID'),
name: t('CLIENTS_HEADING_NAME'),
abbrev: t('CLIENTS_HEADING_ABBREV'),
};
return (
<>
<Breadcrumb items={[{ title: 'BREADCRUMB_CLIENTS' }]}>
<Toolbar
search={params.search}
setSearch={searchHandler}
type={params.type}
setType={typeHandler}
reloadData={reloadData}
/>
</Breadcrumb>
<DataTable
rows={records}
columns={columns}
showActionBtns={true}
deletable={false}
editHandler={id => history.push(`/clients/${id}`)}
totalElements={totalElements}
params={params}
setParams={setParams}
/>
</>
);
};
Component.test.js
const initialState = {
clients: {
list: {
records: companies,
totalElements: 5,
},
},
fields: {
companyTypes: ['All Companies', 'Active Companies', 'Disabled Companies'],
},
};
const middlewares = [thunk];
const mockStoreConfigure = configureMockStore(middlewares);
const store = mockStoreConfigure({ ...initialState });
const originalDispatch = store.dispatch;
store.dispatch = jest.fn(originalDispatch)
// configuring the enzyme we can also configure using Enjym.configure
configure({ adapter: new Adapter() });
describe('Clients ', () => {
let wrapper;
const columns = {
id: i18n.t('CLIENTS_HEADING_ID'),
name: i18n.t('CLIENTS_HEADING_NAME'),
abbrev: i18n.t('CLIENTS_HEADING_ABBREV'),
};
beforeEach(() => {
const historyMock = { push: jest.fn() };
wrapper = mount(
<Provider store={store}>
<Router>
<Clients history={historyMock} />
</Router>
</Provider>
);
});
it('on changing the setSearch of toolbar should call the searchHandler', () => {
const toolbarNode = wrapper.find('Toolbar');
expect(toolbarNode.prop('search')).toEqual('')
act(() => {
toolbarNode.props().setSearch('Hello test');
});
toolbarNode.simulate('change');
****here I want to test dispatch function in useEffect calls with correct params"**
wrapper.update();
const toolbarNodeUpdated = wrapper.find('Toolbar');
expect(toolbarNodeUpdated.prop('search')).toEqual('Hello test')
})
});
[upd] I've changed my mind dramatically since then. Now I think mocking store(with redux-mock-store or even real store that changes its state) - and wrapping component with <Provider store={mockedStore}> - is way more reliable and convenient. Check another answer below.
if you mock react-redux you will be able to verify arguments for useDispatch call. Also in such a case you will need to re-create useSelector's logic(that's really straightforward and actually you don't have to make mock be a hook). Also with that approach you don't need mocked store or <Provider> at all.
import { useSelector, useDispatch } from 'react-redux';
const mockDispatch = jest.fn();
jest.mock('react-redux', () => ({
useSelector: jest.fn(),
useDispatch: () => mockDispatch
}));
it('loads data on init', () => {
const mockedDispatch = jest.fn();
useSelector.mockImplementation((selectorFn) => selectorFn(yourMockedStoreData));
useDispatch.mockReturnValue(mockedDispatch);
mount(<Router><Clients history={historyMock} /></Router>);
expect(mockDispatch).toHaveBeenCalledWith(/*arguments your expect*/);
});
import * as redux from "react-redux";
describe('dispatch mock', function(){
it('should mock dispatch', function(){
//arrange
const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
const mockDispatchFn = jest.fn()
useDispatchSpy.mockReturnValue(mockDispatchFn);
//action
triggerYourFlow();
//assert
expect(mockDispatchFn).toHaveBeenCalledWith(expectedAction);
//teardown
useDispatchSpy.mockClear();
})
}});
From functional component we mock dispatch like above to stop it to execute the real implementation. Hope it helps!
This is how I solved using react testing library:
I have this wrapper to render the components with Provider
export function configureTestStore(initialState = {}) {
const store = createStore(
rootReducer,
initialState,
);
const origDispatch = store.dispatch;
store.dispatch = jest.fn(origDispatch)
return store;
}
/**
* Create provider wrapper
*/
export const renderWithProviders = (
ui,
initialState = {},
initialStore,
renderFn = render,
) => {
const store = initialStore || configureTestStore(initialState);
const testingNode = {
...renderFn(
<Provider store={store}>
<Router history={history}>
{ui}
</Router>
</Provider>
),
store,
};
testingNode.rerenderWithProviders = (el, newState) => {
return renderWithProviders(el, newState, store, testingNode.rerender);
}
return testingNode;
}
Using this I can call store.dispatch from inside the test and check if it was called with the action I want.
const mockState = {
foo: {},
bar: {}
}
const setup = (props = {}) => {
return { ...renderWithProviders(<MyComponent {...props} />, mockState) }
};
it('should check if action was called after clicking button', () => {
const { getByLabelText, store } = setup();
const acceptBtn = getByLabelText('Accept all');
expect(store.dispatch).toHaveBeenCalledWith(doActionStuff("DONE"));
});
I see advantages in using actual <Provider store={store}>:
much easier to write tests
much more readable since just store's data is actually mocked(one mock instead of multiple - and sometimes inconsistent - mocks for useDispatch and useSelector)
But introducing real store with real reducer(s) and real dispatching looks like overkill to me as for unit testing(but would be ok to integration testing):
mocking all the server requests might be a huge task
typically we already have that logic covered with test on per-slice basis
With this in mind have picked configureStore from redux-mock-store instead redux and got next helper(uses Enzyme):
import { act } from 'react-dom/test-utils';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
function renderInRedux(
children,
initialData = {}
) {
let state = initialData;
const store = (configureMockStore([thunk]))(() => state);
const wrapper = mount(
<Provider store={store}>
{children}
</Provider>
);
return {
/*
since Enzyme wrappers are readonly, we need retrieve target element in unit test each time after any interaction
*/
getComponent() {
return wrapper.childAt(0);
},
/*
set store to any desired config; previous value is replaced;
*/
replaceStore(newState) {
act(() => {
state = newState;
store.dispatch({ type: dummyActionTypeName }); // just to trigger listeners
});
wrapper.update();
},
/*
bridge to redux-mock-store's getActions
*/
getActions() {
return store.getActions().filter(({ type }) => type !== dummyActionTypeName);
},
/*
bridge to redux-mock-store's clearActions()
*/
clearActions() {
return store.clearActions();
},
};
}
And example of usage:
const {
getComponent,
replaceStore,
} = renderInRedux(<Loader />, { isRequesting: false });
expect(getComponent().isEmptyRender()).toBeTruthy();
replaceStore({ isRequesting: true });
expect(getComponent().isEmptyRender()).toBeFalsy();
But how would it help to avoid mocking server side interaction if we want to test dispatching? Well, by itself it does not. But we can mock and test action dispatching in easy way:
import { saveThing as saveThingAction } from '../myActions.js';
jest.mock('../myActions.js', () => ({
saveThing: jest.fn().mockReturnValue({ type: 'saveThing' })
}));
beforeEach(() => {
});
....
const { getComponent, getActions } = renderInRedux(
<SomeForm />,
someMockedReduxStore
);
getComponent().find(Button).simulate('click');
expect(getActions()).toContainEqual(saveThingAction());
expect(saveThingAction).toHaveBeenCalledWith(someExpectedArguments);
import * as ReactRedux from 'react-redux'
describe('test', () => {
it('should work', () => {
const mockXXXFn = jest.fn()
const spyOnUseDispatch = jest
.spyOn(ReactRedux, 'useDispatch')
.mockReturnValue({ xxxFn: mockXXXFn })
// Do something ...
expect(mockXXXFn).toHaveBeenCalledWith(...)
spyOnUseDispatch.mockRestore()
})
})
UPDATE: DO NOT use React Redux hooks API which is strongly coupling with Redux store implementation logic, make it very difficult to test.
I'm trying to test my LoginForm component using jest and react-testing-library. When the login form is submitted successfully, my handleLoginSuccess function is supposed to set the 'user' item on localStorage and navigate the user back to the home page using history.push(). This works in my browser in the dev environment, but when I render the component using Jest and mock out the API, localStorage gets updated but the navigation to '/' doesn't happen.
I've tried setting localStorage before calling history.push(). I'm not sure what is responsible for re-rendering in this case, and why it works in dev but not test.
Login.test.jsx
import 'babel-polyfill'
import React from 'react'
import {withRouter} from 'react-router'
import {Router} from 'react-router-dom'
import {createMemoryHistory} from 'history'
import {render, fireEvent} from '#testing-library/react'
import Login from '../../pages/Login'
import API from '../../util/api'
jest.mock('../../util/api')
function renderWithRouter(
ui,
{route = '/', history = createMemoryHistory({initialEntries: [route]})} = {},
) {
return {
...render(<Router history={history}>{ui}</Router>),
// adding `history` to the returned utilities to allow us
// to reference it in our tests (just try to avoid using
// this to test implementation details).
history,
}
}
describe('When a user submits the login button', () => {
test('it allows the user to login', async () => {
const fakeUserResponse = {'status': 200, 'data': { 'user': 'Leo' } }
API.mockImplementation(() => {
return {
post: () => {
return Promise.resolve(fakeUserResponse)
}
}
})
const route = '/arbitrary-route'
const {getByLabelText, getByText, findByText} = renderWithRouter(<Login />, {route})
fireEvent.change(getByLabelText(/email/i), {target: {value: 'email#gmail.com '}})
fireEvent.change(getByLabelText(/password/i), {target: {value: 'Foobar123'}})
fireEvent.click(getByText(/Log in/i))
const logout = await findByText(/Log Out/i)
expect(JSON.parse(window.localStorage.getItem('vector-user'))).toEqual(fakeUserResponse.data.user)
})
})
relevant parts of LoginForm.jsx
class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
disableActions: false,
formErrors: null,
};
}
handleLoginSuccess = () => {
const { loginSuccessCallback, redirectOnLogin, history } = { ...this.props };
if (loginSuccessCallback) {
loginSuccessCallback();
} else {
history.push('/');
}
}
loginUser = ({ user }) => {
localStorage.setItem('vector-user', JSON.stringify(user));
}
handleLoginResponse = (response) => {
if (response.status !== 200) {
this.handleResponseErrors(response.errors);
} else {
this.loginUser(response.data);
this.handleLoginSuccess();
}
}
handleLoginSubmit = (event) => {
event.preventDefault();
const {
disableActions, email, password
} = { ...this.state };
if (disableActions === true) {
return false;
}
const validator = new Validator();
if (!validator.validateForm(event.target)) {
this.handleResponseErrors(validator.errors);
return false;
}
this.setState(prevState => ({ ...prevState, disableActions: true }));
new API().post('login', { email, password }).then(this.handleLoginResponse);
return true;
}
}
Login.jsx
import React from 'react';
import { withRouter, Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import LoginForm from '../components/LoginForm';
class Login extends React.Component {
constructor({ location }) {
super();
const originalRequest = location.state && location.state.originalRequest;
this.state = {
originalRequest
};
}
render() {
const { originalRequest } = { ...this.state };
return (
<div>
<h1>Login</h1>
<LoginForm redirectOnLogin={originalRequest && originalRequest.pathname} />
<Link to="/forgot">Forgot your password?</Link>
</div>
);
}
}
Login.propTypes = {
location: PropTypes.shape({
state: PropTypes.shape({
originalRequest: PropTypes.shape({
pathname: PropTypes.string
})
})
})
};
export default withRouter(Login);
Currently the await findByText() times out.
I think that's because in your tests you're not rendering any Route components. Without those react-router has no way to know what to render when the route changes. It will always render Login.
I'm trying to test that a function gets called with the correct arguments, but because I'm using useDispatch from react-redux, I'm getting the following error: Actions must be plain objects. Use custom middleware for async actions.
I've wrapped my test component in renderWithRedux like it says in the documentation.
Test Setup:
const renderWithRedux = (
ui,
{ initialState, store = createStore(reducer, initialState) } = {}
) => {
return {
...render(<Provider store={store}>{ui}</Provider>),
store,
};
};
const templateId = faker.random.number();
const setup = () => {
const props = {
history: {
push: jest.fn(),
},
};
viewTemplateUrl.mockImplementationOnce(() => jest.fn(() => () => {}));
templatePostThunk.mockImplementationOnce(
jest.fn(() => () => Promise.resolve(templateId))
);
const {
container,
getByText,
getByLabelText,
rerender,
debug,
} = renderWithRedux(<NewTemplateForm {...props} />);
return {
debug,
templateId,
props,
container,
rerender,
getByText,
getByLabelText,
templateNameTextField: getByLabelText('Template Name'),
templateNameInput: getByLabelText('Template Name Input'),
saveTemplateButton: getByText('Save Template'),
cancelButton: getByText('Cancel'),
};
};
Failing test:
test('save template calls handleSubmit, push, and viewTemplateUrl', async () => {
const { templateNameInput, saveTemplateButton, props } = setup();
fireEvent.change(templateNameInput, { target: { value: 'Good Day' } });
fireEvent.click(saveTemplateButton);
await expect(templatePostThunk).toHaveBeenCalledWith({
name: 'Good Day',
});
expect(props.history.push).toHaveBeenCalled();
expect(viewTemplateUrl).toHaveBeenCalledWith({ templateId });
});
It should be passing.
The answer was to mock useDispatch and wrap the parent component in renderWithRedux instead.
import { render, fireEvent } from '#testing-library/react';
import '#testing-library/react/cleanup-after-each';
import { useDispatch } from 'react-redux';
import faker from 'faker';
import { NewTemplateForm } from './NewTemplateForm';
import { templatePostThunk } from '../../../redux/actions/templatePost';
import { viewTemplateUrl } from '../../../utils/urls';
jest.mock('../../../redux/actions/templatePost');
jest.mock('../../../utils/urls');
jest.mock('react-redux');
describe('<NewTemplateForm/> controller component', () => {
useDispatch.mockImplementation(() => cb => cb());
const setup = () => {
const props = {
history: {
push: jest.fn(),
},
};
const templateId = faker.random.number();
viewTemplateUrl.mockImplementationOnce(() => () => {});
templatePostThunk.mockImplementationOnce(() => async () => ({
data: { id: templateId },
}));
const { container, getByText, getByLabelText, rerender, debug } = render(
<NewTemplateForm {...props} />
);
You're trying to dispatch a thunk action creator:
await expect(templatePostThunk).toHaveBeenCalledWith({
name: 'Good Day',
});
But you're setting up the store without the thunk middleware:
store = createStore(reducer, initialState)
You would need to add the thunk middleware to the createStore() call for this to work right.