Unit tests for react-redux using Jest, Enzyme.
In-depth explanations for synchronus action creators, reducers and asynchronus action creators
Note : I think .md file doesnt support JSX syntaxs and rendering rest of js code wierdly, so i used open and close tags for the JSX elements. In real code we used single tag syntax.
A redux action creator generally tested for 2 cases:
- type
- payload
// Action.js
/*
Action for setting H1 element
*/
export const setH1 = text => {
return { type: "SET_H1", payload: text };
};
Simple way (unit tests for one action file)
// Action.spec.js
import { setH1 } from "./Action.js";
/*
test suite for setH1 action creator
*/
describe("setH1", () => {
/**
* checking for its type
*/
it('should have type of "SET_H1"', () => {
expect(setH1().type).toBe("SET_H1");
});
/**
* checking for parameter passing as payload
*/
it("should pass on to payload as we pass in params", () => {
let text = { text: "text" };
expect(setH1(text).payload).toBe(text);
});
});
Generic way (can add all action's tests in 1 file)
// Action.spec.js
import * as acts from "./Action";
/**
* @param actionCreator: action creator that needs to be tested
* @param type: type of action from action creator
* @param payload: payload of action from action creator
* @param ...prarams: any params that need to be passed to action creator
*/
const testActionCreator = (actionCreator, type, payload, ...params) => {
describe(actionCreator.name, () => {
const a = actionCreator(...params);
it("type", () => {
expect(a.type).toBe(type);
});
it("payload", () => {
if (typeof a.payload !== "undefined") {
expect(a.payload).toBe(payload);
}
});
});
};
/**
* test suite for all the action creators (synchronus)
*/
describe("Action Creators", () => {
const sampleText = { text: "text" };
/**
* test suite for setH1 action creators
*/
testActionCreator(acts.setH1, "SET_H1", sampleText, sampleText);
/**
* rest of the action creators test cases follw the same
*/
});
An asynchronus action creator tested for 2 cases
- On promise success
- On promise failure
// Async-action-creator.js
/**
* generic function that creates async action creators
*/
import "whatwg-fetch";
export default (url, body, request, receive, error, cont) => {
const receiveCallback = receive;
const errorCallback = error;
return (dispatch, getState) => {
/**
* If we have a pre-condition to fetching,
* check if we should continue.
*/
if (cont) {
if (!cont(getState())) {
return Promise.resolve();
}
}
/**
* Action: Requesting data..
*/
if (request) {
dispatch(request());
}
/**
* Fetch
*/
let f = fetch(url, Object.assign({}, body)).then(response =>
response.json()
);
/**
* Action: Receiving data.
*/
if (receiveCallback !== null) {
f = f.then(data => {
dispatch(receiveCallback(data ? data : {}));
});
}
/**
* Action: Error receiving data.
*/
if (errorCallback) {
f = f.catch(e => {
dispatch(errorCallback(e.toString()));
});
}
return f;
};
};
// Action.js
import asyncActionCreator from "./Async-action-creator";
/**
* action creator for request type
*/
export const requestSurveyList = () => {
return { type: "REQUEST_SURVEY_LIST" };
};
/**
* action creator for success receive type
*/
export const receiveSurveyList = payload => {
return { type: "RECEIVE_SURVEY_LIST", payload: payload };
};
/**
* action creator for receive error type
*/
export const surveyListError = error => {
return { type: "SURVEY_LIST_ERROR", payload: error };
};
export const fetchSurveyList = () => {
return asyncActionCreator(
"http://placeyoururlhere.com",
{
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8"
}
},
requestSurveyList,
receiveSurveyList,
surveyListError,
state => {
return (
!!state.surveys &&
typeof state.surveys === "object" &&
!!state.surveys.list &&
typeof state.surveys.list === "object" &&
!state.surveys.list.length &&
!state.surveys.listLoading
);
}
);
};
//Async-test.js
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
/**
* Mocks a valid HTTP response to an Ajax request.
* @param {any} status
* @param {any} statusText
* @param {any} response
* @returns
*/
export const mockResponse = (status, statusText, response) => {
return new window.Response(response, {
status: status,
statusText: statusText,
headers: {
"Content-type": "application/json"
}
});
};
/**
* Produces a fake fetch request, then performs tests on the Asynchronous Action.
* @param {Function} asyncAction
* @param {Object} store
* @param {Array<string>} actionTypes
* @param {any} fetch
* @returns
*/
export const testFetch = (asyncAction, store, actionTypes, mockFetch) => {
const globalFetch = global.fetch;
global.fetch = mockFetch;
/**
* Dispatch the asyncAction, then...
*/
store = configureMockStore([thunk])(store);
return store.dispatch(asyncAction).then(() => {
/**
* Count the generated Actions.
*/
const actions = store.getActions();
expect(actions.length).toBe(actionTypes.length);
/**
* Compare each Action type with the expected results.
*/
for (let x = 0; x < actions.length; x++) {
expect(actions[x].type).toBe(actionTypes[x]);
}
global.fetch = globalFetch;
});
};
/**
* Test a dispatched asynchronous action creator against an error response.
*
* @param {Function} asyncAction
* @param {Object} store
* @param {Array<string>} actionTypes
* @param {string} [response='{ one: "two" }']
* @returns
*/
export const testError = (
asyncAction,
store,
actionTypes = [],
response = '{"one":"two"}'
) => {
return testFetch(asyncAction, store, actionTypes, () =>
Promise.reject(mockResponse(500, null, response))
);
};
/**
* Test a dispatched asynchronous action creator against a success response.
*
* @param {Function} asyncAction
* @param {Object} store
* @param {Array<string>} actionTypes
* @param {string} [response='{ one: "two" }']
* @returns
*/
export const testSuccess = (
asyncAction,
store,
actionTypes = [],
response = '{"one":"two"}'
) => {
return testFetch(asyncAction, store, actionTypes, () =>
Promise.resolve(mockResponse(200, null, response))
);
};
//Action.spec.js
import { fetchSurveyList } from "./Action";
import { testError, testSuccess } from "./Async-test";
/**
* initial state taken from its reducer
*/
let INITIAL_SURVEYS_STATE = {
data: {},
list: [],
listError: null,
listLoading: false
};
/**
*SURVEY_LIST
*/
describe("Async Action: SURVEY_LIST", () => {
/**
*Test Success and Error
*/
it("should dispatch the Receive Action on success", () =>
testSuccess(fetchSurveyList(), { surveys: INITIAL_SURVEYS_STATE }, [
"REQUEST_SURVEY_LIST",
"RECEIVE_SURVEY_LIST"
]));
it("should dispatch the Error Action on error", () =>
testError(fetchSurveyList(), { surveys: INITIAL_SURVEYS_STATE }, [
"REQUEST_SURVEY_LIST",
"SURVEY_LIST_ERROR"
]));
/**
* Test Pre-Condition
*/
it("should not dispatch when cached", () =>
testError(fetchSurveyList(), {
surveys: { ...INITIAL_SURVEYS_STATE, list: [1] }
}));
it("should not dispatch when loading", () =>
testError(fetchSurveyList(), {
surveys: { ...INITIAL_SURVEYS_STATE, listLoading: true }
}));
});
We need to test all the cases the reducer returns
// Reducer.js
/**
* always heve a default state oresle reducers
* return undefined, which is a pain to decode
*/
const INITIAL_STATE = {
h1: "",
title: ""
};
/**
* dont mutate the state. create new state and return it..
* rule of reducer
*/
export default function Dom_Reducer(state = INITIAL_STATE, action) {
switch (action.type) {
case "SET_H1":
/**
* change H1
*/
return { ...state, h1: action.payload };
case "SET_TITLE":
/**
* change title
*/
return { ...state, title: action.payload };
default:
return state;
}
}
// Reducer.spec.js
import Dom_Reducer from "./Reducer";
/**
* test suite fro dom reducer
*/
describe("dom_reduecer", () => {
/**
* checking for case: default
*/
it("case: default", () => {
expect(Dom_Reducer(undefined, {})).toEqual({ h1: "", title: "" });
});
/**
* checking for case: SET_H1
*/
it("case: SET_H1", () => {
let h1 = "some random header";
expect(Dom_Reducer(undefined, { type: "SET_H1", payload: h1 })).toEqual({
h1: h1,
title: ""
});
});
/**
* checking for case: SET_TITLE
*/
it("case: SET_TITLE", () => {
let title = "some random title";
expect(
Dom_Reducer(undefined, { type: "SET_TITLE", payload: title })
).toEqual({ h1: "", title: title });
});
});
There are lot of ways testing a component. Below tests are few examples on testing a component.
Ways of importing a component
- As function for statetless components and statefull components.
- As prop for containers with redux states.
// Component1.js
/**
*stateless component
*/
const component1 = (props) => (
/**
*some jsx
*/
)
export default component1;
// Component2.js
import React from 'react';
/**
*stateful component
*/
class component2 extends React.Component {
render() {
/**
* some jsx
*/
}
}
export default component2;
// Component3.js
import React from 'react';
import { connect } from 'react-redux';
/**
*need to add export before class,
*so that we can import only component part
*/
//||||||
//vvvvvv
export class component3 extends React.Component {
render() {
/**
* some jsx
*/
}
}
export default connet()(component3);
// Component.spec.js
import component1 from './Component1';
import component2 from './Component2';
import { Component3 } from './Component3';
/**
* test cases goes here
*/
// Component.js
import React from 'react';
const component = (props) => (
/**
* some jsx
*/
)
export default component;
Note : Dont forget to pass mock props to the component.
// Component.spec.js
import React from 'react';
import renderer from 'react-test-renderer';
import component from './Component.js';
/**
* NOTE:
* this example component doesnt have any props passed to it.
* if a component need props, we need to mock them or atleast define them
* and pass as props to the component. if we sont dont pass the props
* jest will throw errors saying 'not defined' or 'undefined'
*/
/**
* example on how to pass props for this component
* let props = {
* testString: "test",
* testMethod: ()=> {},
* testObject: {},
* testNumber: 1
* };
*
* it('renders correctly', () => {
* let snapshot = renderer.create(<component {...props}/>).toJSON();
* expect(snapshot).toMatchSnapshot();
* });
*/
describe('<component />', () => {
//test spec for comparing with existing snapshot or create one
it('renders correctly', () => {
let snapshot = renderer.create(<component></component>).toJSON();
expect(snapshot).toMatchSnapshot();
});
});
// Component.js
import React from "react";
const SearchComponent = props => {
return (
<div>
<input
onChange={e => props.onChange(e.target.value)}
placeholder={props.placeholder || "Search"}
/>
</div>
);
};
export default SearchComponent;
// Component.spec.js
import React from "react";
/**
* Shallow rendering is useful to constrain yourself to testing a
* component as a unit, and to ensure that your tests aren't
* indirectly asserting on behavior of child components.
*/
import { shallow } from "enzyme";
import SearchComponent from "./Component";
describe("<SearchComponent />", () => {
/**
* we generally create / compare a snapshot here
*/
it("input value change", () => {
/**
* creating a mock function
*/
const myMock = jest.fn();
/**
* passing this mock function to the component as props
* here onChange is a prop for the component
*/
const wrapper = shallow(<SearchComponent onChange={myMock} ></SearchComponent>);
expect(myMock.mock.calls.length).toBe(0);
/**
* simulate a change event so that onChange on input triggers
* and myMock will be called.
* will see about 'simulate' later
*/
/**
* after simulate, onchange triggers and
* myMock will be called once
*/
expect(myMock.mock.calls.length).toBe(1);
/**
* we can also test on what params it was called.
* will do it later
*/
});
});
// Component.js
import React from "react";
const SearchComponent = props => {
return (
<div>
<input
onChange={e => props.onChange(e.target.value)}
placeholder={props.placeholder || "Search"}
/>
</div>
);
};
export default SearchComponent;
// Component.spec.js
import React from "react";
/**
* Shallow rendering is useful to constrain yourself to testing a
* component as a unit, and to ensure that your tests aren't
* indirectly asserting on behavior of child components.
*/
import { shallow } from "enzyme";
import SearchComponent from "./Component";
describe("<SearchComponent />", () => {
/**
* we generally create / compare a snapshot here
*/
it("input value change", () => {
/**
* creating a mock function
*/
const myMock = jest.fn();
/**
* passing this mock function to the component as props
* here onChange is a prop for the component
*/
const wrapper = shallow(<SearchComponent onChange={myMock}></SearchComponent>);
/**
* initially myMock is not called
*/
expect(myMock.mock.calls.length).toBe(0);
/**
* simulate the change event on input and pass the value
* to it
*/
wrapper.find("input").simulate("change", {
target: { value: "test" }
});
/**
* after simulate, onchange triggers and
* myMock will be called once
*/
expect(myMock.mock.calls.length).toBe(1);
/**
* we simulated the change event with value = 'test'
* so the mock function will be called with 'test' param
*/
expect(myMock.mock.calls[0][0]).toBe("test");
});
});
// Component.js
import React from "react";
class LoginComponent extends React.Component {
onFormSubmit(event) {
console.log("onFormSubmit is called");
}
render() {
return (
<div>
<form onSubmit={this.onFormSubmit.bind(this)}>
</form>
</div>
);
}
}
export default LoginComponent;
// Component.spec.js
import React from "react";
/**
* Shallow rendering is useful to constrain yourself to testing a
* component as a unit, and to ensure that your tests aren't
* indirectly asserting on behavior of child components.
*/
import { shallow } from "enzyme";
import LoginComponent from "./Component";
describe("<LoginComponent />", () => {
/**
* we generally create / compare a snapshot here
*/
it("form submit", () => {
/**
* create a spy on onFormSubmit method
*/
const onFormSubmitSpy = jest.spyOn(
LoginComponent.prototype,
"onFormSubmit"
);
let wrapper = shallow(<LoginComponent></LoginComponent>);
/**
* simulate form submit, so that onFormSubmit will invoke
*/
wrapper.find("form").simulate("submit");
/**
* check whether onFormSubmit invokde or not with spy.
*/
expect(onFormSubmitSpy).toHaveBeenCalledTimes(1);
});
});
// Component.js
import React from "react";
class LoginComponent extends React.Component {
onUserKeyPress(event) {
/**
* If the first character is numeric or
* the character is non-alphanumeric, prevent it.
*/
if (
(event.target.value.length === 0 && event.key.match(/\d/)) ||
event.key.match(/[^\da-zA-Z]/)
) {
event.preventDefault();
return false;
}
return true;
}
render() {
return (
<div>
<form></form>
</div>
);
}
}
export default LoginComponent;
// Component.spec.js
import React from "react";
/**
* Shallow rendering is useful to constrain yourself to testing a
* component as a unit, and to ensure that your tests aren't
* indirectly asserting on behavior of child components.
*/
import { shallow } from "enzyme";
import LoginComponent from "./Component";
describe("<LoginComponent />", () => {
/**
* we generally create / compare a snapshot here
*/
it("doesnt accept numbers as first char", () => {
const wrapper = shallow(<LoginComponent></LoginComponent>);
expect(
wrapper.instance().onUserKeyPress({
target: { value: "" },
key: "1",
preventDefault: () => {}
})
).toBe(false);
});
});
// partent.js
class Parent extends React.Component {
constructor(props){
super(props);
}
render() {
return (
<div>
<Child childCallbackProp ={ x => {this.props.parentCallbackProp(x)}}>
</div>
)
}
}
export default Parent
// parent.spec.js
import { shallow } from "enzyme";
import Parent from "./parent.js";
describe("Parent Component", ()=> {
it("testing child-parent callback", ()=> {
const parentCallbackProp_mock = jest.fn();
const prop = {/*All the props the parent depends on*/};
const wrapper = shallow(<Parent {...prop} parentCallbackProp= {parentCallbackProp_mock}/>);
wrapper.find('Child').prop('childCallbackProp')({x: 1, y:2});
expect(parentCallbackProp_mock.mock.calls[0][0]).toEqual({x: 1, y:2});
)
})
import that function
import { uuid } from '../uuid';
mock the file
jest.mock('../uuid');
mock the return values. in this example it has to responsd with 2 unique ids
(uuid as any).mockReturnValueOnce(10)
.mockReturnValueOnce(20)
- All the props used in the component should be either mocked or defined.
- Mock or Spy the functions before shallow or full Dom Rendering (mount) .
- We used mocks for tracking redux actions, Spies for component methods tracking.
- Be careful when
.instance()
and.simulate()
in same test spec. - To test component lifecycle hooks use mount instead shallow. shallow allows few lifecycles in latest version.
- Some times we get errors beacuse 3rd party library components. In this case mock that library in that test file
- like
jest.mock("library-name")
- like
- when using renderer for snapshot testing,
- component has
<Link>
which requires<Router>
as dependency so i wrapped the component in<Router>
<Router>
needs "history" prop. Imported createHistory from "history/createBrowserHistory" and used for history
- component has
- if spying on class methods reset and restore them before each test case,
- if not reset, calls get satcked.
- if not restored, it messed with instance method invokings.
- If you want to test
refs
you have tomount
. - Spying on instance methods needs forceUpdate(). More info