I am learning taking this testing course to test connected components by setting up a store factory
test helper that 'creates a store for testing that matches the configuration of our store'. Below, you can see my connected sample component as well as the code used to setup tests, in which I create a connected, shallow enzyme wrapper of my sample component. However, it seems like the initial state I am passing to the sample component, in this case {jotto: 'foo'}
is not getting passed to my sample component when creating this shallow wrapper. Am I doing something wrong and how can I correctly recreate the necessary store configuration when running enzyme tests? Thanks!
Sample Component:
import React from 'react';
import {connect} from 'react-redux';
const SampleComponent = (props) => {
console.log(props);
return (
<div>This is a sample component!</div>
);
};
const mapStateToProps = (state) => ({
jotto: state.jotto,
});
export default connect(mapStateToProps)(SampleComponent);
reducer:
import * as jottoActionTypes from 'actionTypes/jottoActionTypes';
export const initialState = {
isSuccess: false,
};
const jotto = (state = initialState, action) => {
switch (action.type) {
case jottoActionTypes.CORRECT_GUESS:
return {
...state,
isSuccess: true,
};
default:
return state;
}
};
export default jotto;
root reducer:
import {combineReducers} from 'redux';
import {connectRouter} from 'connected-react-router';
import jotto from 'reducers/jottoReducer';
export default (historyObject) => combineReducers({
jotto,
router: connectRouter(historyObject),
});
Setting up test:
import React from 'react';
import {shallow} from 'enzyme';
import {createStore} from 'redux';
import rootReducer from 'reducers/rootReducer';
import SampleComponent from './sampleComponent';
export const storeFactory = (initialState) => createStore(rootReducer, initialState);
const store = storeFactory({jotto: 'foo'});
const wrapper = shallow(<SampleComponent store={store} />).dive();
console.log(wrapper.debug());
// Result:
{ store:
{ dispatch: [Function: dispatch],
subscribe: [Function: subscribe],
getState: [Function: getState],
replaceReducer: [Function: replaceReducer],
[Symbol(observable)]: [Function: observable] },
jotto: undefined,
dispatch: [Function: dispatch],
storeSubscription:
Subscription {
store:
{ dispatch: [Function: dispatch],
subscribe: [Function: subscribe],
getState: [Function: getState],
replaceReducer: [Function: replaceReducer],
[Symbol(observable)]: [Function: observable] },
parentSub: undefined,
onStateChange: [Function: bound onStateChange],
unsubscribe: [Function: unsubscribe],
listeners:
{ clear: [Function: clear],
notify: [Function: notify],
get: [Function: get],
subscribe: [Function: subscribe] } } }
Just a heads up about that Udemy course... it's not the greatest learning tool. The instructor approaches testing using data attributes
which are unnecessary for jest
and enzyme
testing (they also crowd up the DOM
with unused attributes).
In addition, her code experience is around a beginner level and she makes quite a few mistakes and odd code choices. That said, learn what you can from it and start looking into tests created by those who maintain popular npm packages (most well-documented and popular packages will contain tests that'll teach you a more practical approach of unit
and integration
testing).
Anyway, I digress, you have two options for testing a container
:
export
the class
/pure function
, shallow
or mount
wrap it, and update it with fake props (very easy, less of a headache, and more common to do)
- Wrap your component in a redux
<Provider>
and react-router-dom's <MemoryRouter>
, and then mount
it (can become very complex as it requires a semi-deep understanding of: enzyme and how it interprets the DOM when a component is mounted, redux's action/reducer flow, how to create mock implementations and/or mock files, and how to properly handle promise
based actions).
Working examples (click the Tests
tab to run the tests; locate the .tests.js
in the directories mentioned below):
Note: Codesandbox currently has some testing limitations as noted below, so please adjust for your local project.
containers/Dashboard/__tests__/UnconnectedDashboard.test.js (you can just as easily mount
wrap this unconnected component to assert against its deeply nested children nodes)
import { Dashboard } from "../index.js";
/*
codesandbox doesn't currently support mocking, so it's making real
calls to the API; as a result, the lifecycle methods have been
disabled to prevent this, and that's why I'm manually calling
componentDidMount.
*/
const getCurrentProfile = jest.fn();
const fakeUser = {
id: 1,
name: "Leanne Graham",
username: "Bret",
email: "Sincere@april.biz",
address: {
street: "Kulas Light",
suite: "Apt. 556",
city: "Gwenborough",
zipcode: "92998-3874",
geo: {
lat: "-37.3159",
lng: "81.1496"
}
},
phone: "1-770-736-8031 x56442",
website: "hildegard.org",
company: {
name: "Romaguera-Crona",
catchPhrase: "Multi-layered client-server neural-net",
bs: "harness real-time e-markets"
}
};
const initialProps = {
getCurrentProfile,
currentUser: {},
isLoading: true
};
describe("Unconnected Dashboard Component", () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<Dashboard {...initialProps} />);
wrapper.instance().componentDidMount();
});
afterEach(() => wrapper.unmount());
it("initially renders a spinnner", () => {
expect(getCurrentProfile).toHaveBeenCalled();
expect(wrapper.find("Spinner")).toHaveLength(1);
});
it("displays the current user", () => {
wrapper.setProps({ currentUser: fakeUser, isLoading: false });
expect(getCurrentProfile).toHaveBeenCalled();
expect(wrapper.find("DisplayUser")).toHaveLength(1);
});
it("displays a signup message if no users exist", () => {
wrapper.setProps({ isLoading: false });
expect(getCurrentProfile).toHaveBeenCalled();
expect(wrapper.find("DisplaySignUp")).toHaveLength(1);
});
});
containers/Dashboard/__tests__/ConnectedDashboard.test.js
import Dashboard from "../index";
// import { getCurrentProfile } from "../../../actions/profileActions";
import * as types from "../../../types";
/*
codesandbox doesn't currently support mocking, so it's making real
calls to the API; however, actions like getCurrentProfile, should be
mocked as shown below -- in your case, you wouldn't need to use
a promise, but instead just mock the "guessedWord" action and return
store.dispatch({ ... })
*/
const fakeUser = {
id: 1,
name: "Leanne Graham",
username: "Bret",
email: "Sincere@april.biz",
address: {
street: "Kulas Light",
suite: "Apt. 556",
city: "Gwenborough",
zipcode: "92998-3874",
geo: {
lat: "-37.3159",
lng: "81.1496"
}
},
phone: "1-770-736-8031 x56442",
website: "hildegard.org",
company: {
name: "Romaguera-Crona",
catchPhrase: "Multi-layered client-server neural-net",
bs: "harness real-time e-markets"
}
};
const flushPromises = () => new Promise(resolve => setImmediate(resolve));
describe("Connected Dashboard Component", () => {
let store;
let wrapper;
beforeEach(() => {
store = createStoreFactory();
wrapper = mount(
<Provider store={store}>
<MemoryRouter>
<Dashboard />
</MemoryRouter>
</Provider>
);
});
afterEach(() => wrapper.unmount());
it("initially displays a spinner", () => {
expect(wrapper.find("Spinner")).toHaveLength(1);
});
it("displays the current user after a successful API call", async () => {
/*
getCurrentProfile.mockImplementationOnce(() => new Promise(resolve => {
resolve(
store.dispatch({
type: types.SET_SIGNEDIN_USER,
payload: fakeUser
})
);
});
await flushPromises();
wrapper.update();
expect(wrapper.find("DisplayUser")).toHaveLength(1);
*/
store.dispatch({
type: types.SET_SIGNEDIN_USER,
payload: fakeUser
});
wrapper.update();
expect(wrapper.find("DisplayUser")).toHaveLength(1);
});
it("displays a signup message if no users exist", async () => {
/*
getCurrentProfile.mockImplementationOnce(() => new Promise((resolve,reject) => {
reject(
store.dispatch({
type: types.FAILED_SIGNEDIN_USER
})
);
});
await flushPromises();
wrapper.update();
expect(wrapper.find("DisplaySignUp")).toHaveLength(1);
*/
store.dispatch({
type: types.FAILED_SIGNEDIN_USER
});
wrapper.update();
expect(wrapper.find("DisplaySignUp")).toHaveLength(1);
});
});
Solution: I forgot the browser parameter for my root reducer, given I was using connected-react-router
.
import rootReducer from 'reducers/rootReducer';
import {createBrowserHistory} from 'history';
export const storeFactory = (initialState) => createStore(rootReducer(createBrowserHistory()), initialState);