I am using mapDispatchToProps to make a dispatch call to an API in the useEffect of my functional component.
I am a little stuck as how to actually test this in my unit tests with React Testing Library.
I can pass my Redux store quite easily, however I don't think I've ever had to pass the dispatch before and I'm a little lost.
I did try to pass the dispatch function with my store, but this of course didn't work.
Component
const mapDispatchToProps = dispatch => ({
myDispatchFunction: () => dispatch(someDispatch())
});
const mapStateToProps = ({someStateProp}) => ({
myStateProp: !!someStateProp // This isn't important
});
const MyComp = ({myDispatchFunction}) => {
useEffect(() => {
!!myStateProp && myDispatchFunction();
}, []);
return ...
}
Test
it('Should trigger dispatch function on load', () => {
const mockFunc = jest.fn(); // My attempt at mocking the dispatch call
const store = {someStateProp: true, myDispatchFunction: mockFunc};
render(
<Provider store={mockStore(store)}>
<MyComponent />
</Provider>
);
expect(mockFunc).toHaveBeenCalled();
});
This fails...
Your code has no real meaning, but the same with the test method, try not to mock functions and modules that have no side effects, I/O operations, and use their original implementation.
For your example, don't mock mapStateToProps, mapDispatchToProps, and dispatch functions. We can create a test store, populate the test state data, and collect the dispatch actions in the component.
If your component uses some state slice, verify that your component is rendering correctly. This is also called behavior testing. This testing strategy is more robust if the component behaves correctly, regardless of whether your implementation changes.
Why not mock? Like mapStateToProps, where a mock implementation changes its behavior if you don't know how it is implemented, resulting in an error-based implementation of the test case. Your tests may pass but the actual code at runtime is not correct.
E.g.
index.tsx:
import { useEffect } from 'react';
import { connect } from 'react-redux';
const mapDispatchToProps = (dispatch) => ({
myDispatchFunction: () => dispatch({ type: 'SOME_ACTION' }),
});
const mapStateToProps = ({ someStateProp }) => ({
myStateProp: !!someStateProp,
});
const MyComp = ({ myDispatchFunction, myStateProp }) => {
useEffect(() => {
!!myStateProp && myDispatchFunction();
}, []);
return null;
};
export default connect(mapStateToProps, mapDispatchToProps)(MyComp);
index.test.tsx:
import { render } from '#testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import MyComp from './';
describe('71555438', () => {
test('should pass', () => {
let dispatchedActions: any[] = [];
const store = createStore(function rootReducer(state = { someStateProp: 'fake value' }, action) {
if (!action.type.startsWith('##redux')) {
dispatchedActions.push(action);
}
return state;
});
render(
<Provider store={store}>
<MyComp />
</Provider>
);
expect(dispatchedActions).toEqual([{ type: 'SOME_ACTION' }]);
});
});
Test result:
PASS stackoverflow/71555438/index.test.tsx (9.562 s)
71555438
✓ should pass (16 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.tsx | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 10.565 s
Note: We should ignore the action dispatched by createStore internally and we only need to collect the actions dispatched by users(our code).
Related
I have made a dummy component for the purpose of illustration. Please find the code below-
Fetch.js
import React from "react";
import { useFetch } from "./useFetch";
const FetchPost = () => {
const { toShow, dummyAPICall } = useFetch();
return toShow ? (
<>
<button onClick={dummyAPICall} data-testid="fetch-result">
Fetch
</button>
</>
) : null;
};
export { FetchPost };
useFetch.js
import { useState } from "react";
const useFetch = () => {
const [toShow, setToShow] = useState(true);
const dummyAPICall = () => {
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.json())
.then((json) => {
setToShow(false);
})
.catch(() => {
setToShow(true);
});
};
return {
toShow,
setToShow,
dummyAPICall,
};
};
export { useFetch };
I want to make an assertion here, that on click of the Fetch button my Fetch component shouldn't render on screen so, using React Testing Library, I am writing the test case like this:
import { fireEvent, render, screen } from "#testing-library/react";
import { FetchPost } from "../Fetch";
import { useFetch } from "../useFetch";
jest.mock("../useCounter");
describe("Use Fetch tests", () => {
it("Should fetch results and show/hide component", async () => {
useFetch.mockImplementation(() => {
return { toShow: true, dummyAPICall: jest.fn() };
});
render(<FetchPost></FetchPost>);
expect(screen.getByText(/fetch/i)).toBeInTheDocument();
fireEvent.click(screen.getByTestId("fetch-result"));
await waitFor(() =>
expect(screen.queryByText(/fetch/i)).not.toBeInTheDocument()
);
});
});
My assertion:
expect(screen.getByText(/fetch/i)).not.toBeInTheDocument();
is failing as the component is still present. How can I modify my test case to handle this?
Mock network IO side effect is better than mock useFetch hook. It seems you forget to import waitFor helper function from RTL package.
There are two options to mock network IO side effects.
msw, here is example
global.fetch = jest.fn(), don't need to install additional package and set up.
I am going to use option 2 to solve your question.
E.g.
fetch.jsx:
import React from 'react';
import { useFetch } from './useFetch';
const FetchPost = () => {
const { toShow, dummyAPICall } = useFetch();
return toShow ? (
<>
<button onClick={dummyAPICall} data-testid="fetch-result">
Fetch
</button>
</>
) : null;
};
export { FetchPost };
useFetch.js:
import { useState } from 'react';
const useFetch = () => {
const [toShow, setToShow] = useState(true);
const dummyAPICall = () => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => response.json())
.then((json) => {
setToShow(false);
})
.catch(() => {
setToShow(true);
});
};
return { toShow, setToShow, dummyAPICall };
};
export { useFetch };
fetch.test.jsx:
import React from 'react';
import { FetchPost } from './fetch';
import { fireEvent, render, screen, waitFor } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
describe('Use Fetch tests', () => {
it('Should fetch results and show/hide component', async () => {
const mResponse = { json: jest.fn() };
global.fetch = jest.fn().mockResolvedValueOnce(mResponse);
render(<FetchPost />);
expect(screen.getByText(/fetch/i)).toBeInTheDocument();
fireEvent.click(screen.getByTestId('fetch-result'));
await waitFor(() => expect(screen.queryByText(/fetch/i)).not.toBeInTheDocument());
});
});
Test result:
PASS examples/70666232/fetch.test.jsx (12.699 s)
Use Fetch tests
✓ Should fetch results and show/hide component (51 ms)
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 93.75 | 100 | 83.33 | 93.75 |
fetch.jsx | 100 | 100 | 100 | 100 |
useFetch.js | 90 | 100 | 80 | 90 | 12
-------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 14.38 s
package version:
"#testing-library/react": "^11.2.2",
"#testing-library/jest-dom": "^5.11.6",
"jest": "^26.6.3",
"react": "^16.14.0"
I want to test if the component is dispatching an action to the store, for this, i have to mock the functionalities of React Hooks.
I'm using the useDispatch hook to get the dispatcher function and put inside a variable so them i can call it.
When i run my test suits, even if the useDispatch function is mocked, it returns an error saying that it's not a function.
jest.mock('react-redux', () => ({
useSelector: jest.fn(),
useDispatch: jest.fn(() => {}),
}));
it('should be able open modal', () => {
const { getByTestId } = render(<Dropdown />);
fireEvent.click(getByTestId('dropdown'));
fireEvent.click(getByTestId('button-Transactions'));
const dispatch = jest.fn();
useDispatch.mockReturnValue(dispatch);
expect(dispatch).toHaveBeenCalledWith(openModal('transactions'));
});
The error:
TypeError: dispatch is not a function
19 |
20 | if (item.action) {
> 21 | dispatch(item.action);
| ^
22 | }
23 |
24 | return item;
My component:
const history = useHistory();
const dispatch = useDispatch();
function handleNavigation(item) {
if (item.path) return history.push(item.path);
if (item.action) {
dispatch(item.action);
}
return item;
}
The component was trying to execute a function that wasn't declared yet.
We need to mock before of the render method
const dispatch = jest.fn();
useDispatch.mockReturnValue(jest.fn());
const dropdown = render(<Dropdown />);
It is because when you mocked it, you haven't specified a value.
I thought of two ways to do this:
Having a real store in your test, so you can test the integration:
const mockedStore = { /* the things you need in store in your test */ };
const store = configureStore(mockedStore, browserHistory);
const { getByTestId } = render(<Provider store={store}><Dropdown /></Provider>);
Mock your dispatch, but you ll end up having infinite issues with useSelector (especially if you have more than one useSelector in the tree you rendered).
import * as ReactRedux from 'react-redux';
// useDispatch returns a function which we are mocking here
const mockDispatch = jest.fn();
beforeAll(() => {
ReactRedux.useDispatch = jest.fn().mockImplementation(() => mockDispatch);
});
beforeEach(() => {
ReactRedux.useDispatch.mockClear();
});
expect(mockDispatch).toHaveBeenCalledWith(yourAction);
Please note that the real issue you will face is not with dispatch, but with useSelector.
If you mock it, it will return the value you want. But what if you have more than one in your tree ? for that, I real store is necessary as far as I know.
When I create a test for my connected React component where I want to test the mapStateToProps logic I run into a problem which I'm not sure how to solve.
Error message
Expected: 1
Received: undefined
24 | it('should show previously rolled value', () => {
25 | // test that the state values were correctly passed as props
> 26 | expect(wrapper.props().lastRolledNumber).toBe(1);
When I check the wrapper.props() it only returns the store object and no properties.
To make sure it's not my code I have found an example that should work to simplify it, but I get the same problem when using that exact version in my app (option #2, https://jsramblings.com/2018/01/15/3-ways-to-test-mapStateToProps-and-mapDispatchToProps.html)
I think it might have to do with the React 16+ version, which I found mentioned here: https://airbnb.io/enzyme/docs/api/ReactWrapper/props.html
.props() => Object
Returns the props object for the root node of the wrapper. It must be
a single-node wrapper. This method is a reliable way of accessing the
props of a node; wrapper.instance().props will work as well, but in
React 16+, stateless functional components do not have an instance.
See .instance() => ReactComponent
Does anyone know how to test this in a good way to see that the logic is working as expected without exporting the private mapStateToProps function directly?
Component
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
// Component 1 - "Base component"
// Exporting it is a good practice for testing its own logic
export const Dice = ({ lastRolledNumber, onRollDice }) => (
<div>
<p>The last rolled number was {lastRolledNumber}.</p>
<button onClick={onRollDice}>Roll dice</button>
</div>
);
Dice.propTypes = {
lastRolledNumber: PropTypes.number.isRequired,
onRollDice: PropTypes.func.isRequired
}
const mapStateToProps = (state) => ({
lastRolledNumber: state.lastRolledNumber
});
const mapDispatchToProps = (dispatch) => ({
onRollDice: () => dispatch({ type: 'ROLL_DICE' })
});
// Component 2 - Container component
// Export it as a default export
export default connect(mapStateToProps, mapDispatchToProps)(Dice);
Test
import React from 'react';
import { shallow } from 'enzyme';
import '../test-config'; // Setup Enzyme & Adapter
import DiceContainer from './Dice';
// Create the mock store
import configureMockStore from 'redux-mock-store';
const mockStore = configureMockStore();
describe('Dice', () => {
let wrapper, store;
beforeEach(() => {
const initialState = {
lastRolledNumber: 1
};
store = mockStore(initialState);
// Shallow render the container passing in the mock store
wrapper = shallow(
<DiceContainer store={store} />
);
});
it('should show previously rolled value', () => {
// test that the state values were correctly passed as props
expect(wrapper.props().lastRolledNumber).toBe(1);
});
it('should roll the dice again when button is clicked', () => {
// test that the component events dispatch the expected actions
wrapper.simulate('rollDice');
const actions = store.getActions();
expect(actions).toEqual([ { type: 'ROLL_DICE' } ]);
});
});
You are almost there. You need to call .dive() method so that you can get the Dice component rather than the DiceContainer component which is wrapped by the connect HOC of react-redux module.
Short answer:
wrapper = shallow(<DiceContainer store={store} />).dive();
A Completed working example:
index.jsx:
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
export const Dice = ({ lastRolledNumber, onRollDice }) => (
<div>
<p>The last rolled number was {lastRolledNumber}.</p>
<button onClick={onRollDice}>Roll dice</button>
</div>
);
Dice.propTypes = {
lastRolledNumber: PropTypes.number.isRequired,
onRollDice: PropTypes.func.isRequired,
};
const mapStateToProps = (state) => ({
lastRolledNumber: state.lastRolledNumber,
});
const mapDispatchToProps = (dispatch) => ({
onRollDice: () => dispatch({ type: 'ROLL_DICE' }),
});
export default connect(mapStateToProps, mapDispatchToProps)(Dice);
index.test.jsx:
import React from 'react';
import { shallow } from 'enzyme';
import configureMockStore from 'redux-mock-store';
import DiceContainer from '.';
const mockStore = configureMockStore();
describe('Dice', () => {
let wrapper;
let store;
beforeEach(() => {
const initialState = {
lastRolledNumber: 1,
};
store = mockStore(initialState);
wrapper = shallow(<DiceContainer store={store} />).dive();
});
it('should show previously rolled value', () => {
expect(wrapper.props().lastRolledNumber).toBe(1);
});
it('should roll the dice again when button is clicked', () => {
wrapper.simulate('rollDice');
const actions = store.getActions();
expect(actions).toEqual([{ type: 'ROLL_DICE' }]);
});
});
Unit test results:
PASS src/stackoverflow/59771991/index.test.jsx (9.645s)
Dice
✓ should show previously rolled value (19ms)
✓ should roll the dice again when button is clicked (2ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 11.505s
Test coverage html report:
In my case, I was able to write the test with render by this -
import React from 'react';
import '#testing-library/jest-dom';
import {
screen, cleanup, render,
} from '#testing-library/react';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
const store = configureMockStore()({
headerState: {
showBack: false,
},
});
const mockProps = { ... };
describe('Component', () => {
beforeAll(() => {
render(
<Provider store={store}>
<Component {...mockProps} />
</Provider>,
);
});
afterAll(cleanup);
test('Test', () => {
screen.debug();
});
});
So I was trying to create a unit test using jest in ReactJS. The Unit test itself just to verify if the function (from action) has been called
I already tried to mock the function, but the result tells that I must mock the function
Here the code of the function that I want to create a unit test
import { testfunction } from '../../actions/auth';
handleSubmit(userParams) {
this.setState({ form: { ...this.state.form, isFetching: true } });
this.props.dispatch(testfunction(userParams,
this.successCallback.bind(this), this.errorCallback.bind(this)));
}
and for the unit test
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import Login from '../../../components/auth/Login';
const mockStore = configureStore([thunk]);
const initialState = {
history: { },
};
const store = mockStore(initialState);
let wrapper;
let history;
let testfunction;
beforeEach(() => {
testfunction= jest.fn();
history = { push: jest.fn() };
wrapper = shallow(
<Login
history={history}
store={store}
testfunction={testfunction}
/>
);
});
describe('handleSubmit()', () => {
test('should call testfunction props', () => {
const component = wrapper.dive();
const instance = component.instance();
const sampleUserParams = {
email: 'test#test.com',
password: 'samplePassword123',
};
instance.handleSubmit(sampleUserParams);
expect(testfunction).toHaveBeenCalled();
});
});
I just want to check if the "testfunction" is called when I called handleSubmit function. But the error message is:
"Expected mock function to have been called."
it feels my way to mock the function is wrong. Does anyone know how to correct way to test that function?
Here is the solution:
index.tsx:
import React, { Component } from 'react';
import { testfunction } from './testfunction';
class Login extends Component<any, any> {
constructor(props) {
super(props);
this.state = {
form: {}
};
}
public render() {
const userParams = {};
return (
<div className="login">
<form onSubmit={() => this.handleSubmit(userParams)}>some form</form>
</div>
);
}
private handleSubmit(userParams) {
this.setState({ form: { ...this.state.form, isFetching: true } });
this.props.dispatch(testfunction(userParams, this.successCallback.bind(this), this.errorCallback.bind(this)));
}
private successCallback() {
console.log('successCallback');
}
private errorCallback() {
console.log('errorCallback');
}
}
export { Login };
testFunction.ts:
async function testfunction(userParams, successCallback, errorCallback) {
return {
type: 'ACTION_TYPE',
payload: {}
};
}
export { testfunction };
Unit test:
import React from 'react';
import { shallow } from 'enzyme';
import { Login } from './';
import { testfunction } from './testfunction';
jest.mock('./testfunction.ts');
describe('Login', () => {
const dispatch = jest.fn();
const sampleUserParams = {
email: 'test#test.com',
password: 'samplePassword123'
};
it('handleSubmit', () => {
const wrapper = shallow(<Login dispatch={dispatch} />);
expect(wrapper.is('.login')).toBeTruthy();
expect(wrapper.find('form')).toHaveLength(1);
wrapper.find('form').simulate('submit');
const cmpInstance = wrapper.instance();
expect(dispatch).toBeCalledWith(
// tslint:disable-next-line: no-string-literal
testfunction(sampleUserParams, cmpInstance['successCallback'], cmpInstance['errorCallback'])
);
// tslint:disable-next-line: no-string-literal
expect(testfunction).toBeCalledWith(sampleUserParams, cmpInstance['successCallback'], cmpInstance['errorCallback']);
});
});
Unit test with coverage report:
PASS src/stackoverflow/57847401/index.spec.tsx
Login
✓ handleSubmit (22ms)
-----------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------------|----------|----------|----------|----------|-------------------|
All files | 86.36 | 100 | 62.5 | 85 | |
index.tsx | 90 | 100 | 71.43 | 88.89 | 27,30 |
testfunction.ts | 50 | 100 | 0 | 50 | 2 |
-----------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.201s, estimated 4s
Here is the completed demo: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57847401
Wondering if someone can point out what I expect is a stupid mistake.
I have an action for user login.
I'm trying to test this action, I've followed the redux documentation as well as the redux-mock-store documentation however I keep getting an error as follows:
TypeError: Cannot read property 'default' of undefined
4 | import thunkMiddleware from 'redux-thunk'
5 |
> 6 | const middlewares = [thunkMiddleware] // add your middlewares like `redux-thunk`
| ^
7 | const mockStore = configureStore(middlewares)
8 |
9 | describe("userActions", () => {
at Object.thunkMiddleware (actions/user.actions.spec.js:6:22)
My test code is as follows:
import {userActions} from "./user.actions";
import {userConstants} from "../constants/user.constants";
import configureStore from 'redux-mock-store'
import thunkMiddleware from 'redux-thunk'
const middlewares = [thunkMiddleware] // add your middlewares like `redux-thunk`
const mockStore = configureStore(middlewares)
describe("userActions", () => {
describe("login", () => {
it(`should dispatch a ${userConstants.LOGIN_REQUEST}`, () =>{
const store = mockStore({});
return store.dispatch(userActions.login("someuser", "somepassword")).then(() => {
expect(store.getState().loggingIn).toBeTruthy();
});
})
})
});
I've double checked both redux-thunk and redux-mock-store are included in my npm dev dependencies as well as deleteing the node_modules directory and reinstalling them all with npm install.
Can anyone see what's going wrong?
Thanks
EDIT:
It seems i'm doing something fundamentally wrong, I've tried to simplify it almost back to a clean slate to find where the problem is introduced.
Even with this test:
import authentication from "./authentication.reducer";
import { userConstants } from "../constants/user.constants";
describe("authentication reducer", () => {
it("is a passing test", () => {
authentication();
expect("").toEqual("");
});
});
Against this:
function authentication(){
return "test";
}
export default authentication
I'm getting an undefined error:
● authentication reducer › is a passing test
TypeError: Cannot read property 'default' of undefined
6 |
7 | it("is a passing test", () => {
> 8 | authentication();
| ^
9 | expect("").toEqual("");
10 | });
at Object.<anonymous> (reducers/authentication.reducer.spec.js:8:9)
yes, according to that error, seems you have a problem with module dependencies. Take a look at your webpack configuration.
Concerning the redux-mock-store, I suggest you to create a helper for future testing needs:
import configureStore from 'redux-mock-store'
import thunk from 'redux-thunk'
export default function(middlewares = [thunk], data = {}) {
const mockedStore = configureStore(middlewares)
return mockedStore(data)
}
and you will include it in your test cases and use like that:
beforeEach(() => {
store = getMockStore()
})
afterEach(() => {
store.clearActions()
})
If you wont test redux with thunk you can use redux-thunk-tester module for it.
Example:
import React from 'react';
import {createStore, applyMiddleware, combineReducers} from 'redux';
import {asyncThunkWithRequest, reducer} from './example';
import ReduxThunkTester from 'redux-thunk-tester';
import thunk from 'redux-thunk';
const createMockStore = () => {
const reduxThunkTester = new ReduxThunkTester();
const store = createStore(
combineReducers({exampleSimple: reducer}),
applyMiddleware(
reduxThunkTester.createReduxThunkHistoryMiddleware(),
thunk
),
);
return {reduxThunkTester, store};
};
describe('Simple example.', () => {
test('Success request.', async () => {
const {store, reduxThunkTester: {getActionHistoryAsync, getActionHistoryStringifyAsync}} = createMockStore();
store.dispatch(asyncThunkWithRequest());
const actionHistory = await getActionHistoryAsync(); // need to wait async thunk (all inner dispatch)
expect(actionHistory).toEqual([
{type: 'TOGGLE_LOADING', payload: true},
{type: 'SOME_BACKEND_REQUEST', payload: 'success response'},
{type: 'TOGGLE_LOADING', payload: false},
]);
expect(store.getState().exampleSimple).toEqual({
loading: false,
result: 'success response'
});
console.log(await getActionHistoryStringifyAsync({withColor: true}));
});
});