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} ... />);
Related
Here is my test
const initialRootState = {
accounts: [mockAccounts],
isLoading: false
}
describe('Account Dashboard', () => {
let rootState = {
...initialRootState
}
const mockStore = configureStore()
const store = mockStore({ ...rootState })
const mockFunction = jest.fn()
jest.spyOn(Redux, 'useDispatch').mockImplementation(() => mockFunction)
jest
.spyOn(Redux, 'useSelector')
.mockImplementation((state) => state(store.getState()))
afterEach(() => {
mockFunction.mockClear()
// Reseting state
rootState = {
...initialRootState
}
})
it('renders correctly', () => {
const wrapper = mount(
<TestWrapper>
<AccountDashboard />
</TestWrapper>
)
console.log(wrapper)
})
})
In my component I am mapping accounts from the state. In my test I am getting the following error TypeError: Cannot read property 'map' of undefined
I would like to test an if statement I am using in my component to ensure it's returning the proper view based on the number of accounts I am receiving.
However, when I console.log(store.getState()) it is printing correctly. What am I doing wrong?
If you're going to test a Redux connected component, I'd recommend steering away from mocking its internals and instead to test it as if it were a React component connected to a real Redux store.
For example, here's a factory function for mounting connected components with enzyme:
utils/withRedux.jsx
import * as React from "react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { mount } from "enzyme";
import rootReducer from "../path/to/reducers";
/*
You can skip recreating this "store" by importing/exporting
the real "store" from wherever you defined it in your app
*/
export const store = createStore(rootReducer);
/**
* Factory function to create a mounted Redux connected wrapper for a React component
*
* #param {ReactNode} Component - the Component to be mounted via Enzyme
* #function createElement - Creates a wrapper around the passed in component with incoming props so that we can still use "wrapper.setProps" on the root
* #returns {ReactWrapper} - a mounted React component with a Redux store.
*/
export const withRedux = Component =>
mount(
React.createElement(props => (
<Provider store={store}>
{React.cloneElement(Component, props)}
</Provider>
)),
options
);
export default withRedux;
Now, using the above factory function, we can test the connected component by simply using store.dispatch:
tests/ConnectedComponent.jsx
import * as React from "react";
import withRedux, { store } from "../path/to/utils/withRedux";
import ConnectedComponent from "../index";
const fakeAccountData = [{...}, {...}, {...}];
describe("Connected Component", () => {
let wrapper;
beforeEach(() => {
wrapper = withRedux(<ConnectedComponent />);
});
it("initially shows a loading indicator", () => {
expect(wrapper.find(".loading-indicator")).exists().toBeTruthy();
});
it("displays the accounts when data is present", () => {
/*
Ideally, you'll be dispatching an action function for simplicity
For example: store.dispatch(setAccounts(fakeAccountData));
But for ease of readability, I've written it out below.
*/
store.dispatch({ type: "ACCOUNTS/LOADED", accounts: fakeAccountData }));
// update the component to reflect the prop changes
wrapper.update();
expect(wrapper.find(".loading-indicator")).exists().toBeFalsy();
expect(wrapper.find(".accounts").exists()).toBeTruthy();
});
});
This vastly simplifies not having to mock the store/useSelector/useDispatch over and over when you start to test other Redux connected components.
On a side note, you can skip this entirely if you use react-redux's connect function while exporting the unconnected component. Instead of importing the default export, you can import the unconnected component within your test...
Example component:
import * as React from "react";
import { connect } from "react-redux";
export const Example = ({ accounts, isLoading }) => { ... };
const mapStateToProps = state => ({ ... });
const mapDispatchToProps = { ... };
export default connect(mapStateToProps, mapDispatchToProps)(Example);
Example test:
import * as React from "react";
import { mount } from "enzyme";
import { Example } from "../index";
const initProps = {
accounts: [],
isLoading: true
};
const fakeAccountData = [{...}, {...}, {...}];
describe("Unconnected Example Component", () => {
let wrapper;
beforeEach(() => {
wrapper = mount(<Example {...initProps } />);
});
it("initially shows a loading indicator", () => {
expect(wrapper.find(".loading-indicator")).exists().toBeTruthy();
});
it("displays the accounts when data is present", () => {
wrapper.setProps({ accounts: fakeAccountData, isLoading: false });
wrapper.update();
expect(wrapper.find(".loading-indicator")).exists().toBeFalsy();
expect(wrapper.find(".accounts").exists()).toBeTruthy();
});
});
I figured out that my test was working incorrectly due to my selector function in my component being implement incorrectly. So the test was actually working properly!
Note: My team is currently using mocked data(waiting for the API team to finish up endpoints).
Originally the useSelector function in my component(that I was testing) looked like:
const { accounts, isLoading } = useSelector(
(state: RootState) => state.accounts,
shallowEqual
)
When I updated this to:
const { accounts, isAccountsLoading } = useSelector(
(state: RootState) => ({
accounts: state.accounts.accounts,
isAccountsLoading: state.accounts.isLoading
}),
shallowEqual
)
My tests worked - here are my final tests:
describe('App', () => {
let rootState = {
...initialState
}
const mockStore = configureStore()
const store = mockStore({ ...rootState })
jest.spyOn(Redux, 'useDispatch').mockImplementation(() => jest.fn())
jest
.spyOn(Redux, 'useSelector')
.mockImplementation((state) => state(store.getState()))
afterEach(() => {
jest.clearAllMocks()
// Resetting State
rootState = {
...initialState
}
})
it('renders correctly', () => {
const wrapper = mount(
<TestWrapper>
<Dashboard />
</TestWrapper>
)
expect(wrapper.find('[data-test="app"]').exists()).toBe(true)
expect(wrapper.find(verticalCard).exists()).toBe(false)
expect(wrapper.find(horizontalCard).exists()).toBe(true)
})
it('renders multiple properly', () => {
rootState.info = mockData.info
const wrapper = mount(
<TestWrapper>
<Dashboard />
</TestWrapper>
)
expect(wrapper.find(verticalCard).exists()).toBe(true)
expect(wrapper.find(horizontalCard).exists()).toBe(false)
})
})
I have a container that I want to connect to the redux store using redux connect function. The connect function typings seems to be able to determine the resulting props, but If I try to access the inferred props on the wrapped component I get typescript errors telling me that not the mapped actions nor the stats state are present on the intersection type connect generates:
Property 'actions' does not exist on type 'PropsWithChildren<Matching<{ stats: { getSessionsPending: boolean; getSessionsError: null; }; } & { actions: { setupApp: () => (dispatch: any, getState: any) => Promise<void>; getSessions: () => GetSessionThunk; dismissGetSessionsError: () => { ...; }; }; }, Matching<...> | (ClassAttributes<...> & Matching<...>)>>'.
Property 'actions' does not exist on type 'Matching<{ stats: { getSessionsPending: boolean; getSessionsError: null; }; } & { actions: { setupApp: () => (dispatch: any, getState: any) => Promise<void>; getSessions: () => GetSessionThunk; dismissGetSessionsError: () => { ...; }; }; }, Matching<...>> & { ...; }
This is a small reproduction example with a link to a typescript playground:
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
const setupApp = () => ({type: 'setup-ok'})
const Dashboard = () => <div>Hello</div>
function mapStateToProps(state:{stats: any}) {
return {
stats: state.stats,
};
}
const mapDispatchToProps = {
actions: { setupApp },
};
const connector = connect(mapStateToProps, mapDispatchToProps);
const DefaultPage = connector(props => {
useEffect(() => {
const { setupApp, getSessions } = props.actions;
setupApp().then(getSessions);
}, []);
const { sessions } = props.stats;
return <Dashboard sessions={sessions} />;
});
export default DefaultPage
Your use of mapDispatch here is actually incorrect and broken.
mapDispatch can either be an object containing action creators, or a function that receives dispatch as an argument. What you have there is neither of those - it's an object that has a nested object containing action creators, and that will not be handled correctly.
So, you either need to switch to doing const mapDispatch = {setupApp} and drop the actions nesting, or you need to write mapDispatch as a function and handle setting up the setupApp prop yourself.
You're right that we do recommend using the ConnectedProps<T> approach if you're using connect with TypeScript. However, we recommend using the React-Redux hooks API as the default today instead of connect, and one of the reasons is that it's way easier to use that with TS.
All that could be simplified down to:
const DefaultPage = () => {
const stats= useSelector( (state: RootState) => state.stats);
const dispatch = useDispatch();
useEffect(() => {
dispatch(setupApp());
}, [dispatch])
}
It seems that, for some reason, the types of the connect function doesn't play very well with actually passing a function taking the arguments it provides (don't ask me why)
However, it is possible to use the connector function (which is a bit alien to normal redux developers) to infer it's props type using a helper type from redux ConnectedProps so all you have to do is type your params as the result of applying ConnectedProps to your connector function:
import React, { useEffect } from 'react';
import { connect, ConnectedProps } from 'react-redux';
const setupApp = () => ({type: 'setup-ok'})
const Dashboard = () => <div>Hello</div>
function mapStateToProps(state:{stats: any}) {
return {
stats: state.stats,
};
}
const mapDispatchToProps = {
actions: { setupApp },
};
const connector = connect(mapStateToProps, mapDispatchToProps);
const DefaultPage = (props: ConnectedProps<typeof connector>) => {
useEffect(() => {
const { setupApp } = props.actions;
setupApp()
}, []);
const { sessions } = props.stats;
return <Dashboard sessions={sessions} />;
}
export default connector(DefaultPage)
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 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.
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.