mobile menu icon

Testing tricks for react-redux

Published by Carlos Blé on 08/06/2017

React, Redux, JavaScript, Testing


We are using enzyme and jsdom to write both, integration and unit tests for react-redux apps. Although enzyme’s shallow is very convenient totest components in isolation, the truth is that often those components have children that need to be rendered in order for the parent to do something meaningful so we end up using enzyme’s mount most of the time.

When there is no logic in the component, I want to test the integration with actions, reducers and the store. Otherwise tests don’t provide me with the sense of safety that I am looking for. The top one difficulty when integrating these pieces is the asynchronous nature of redux - tests finish before the expected behavior occurs.

Using the “done()” parameter provided by mocha or jest plus “setTimeout()” in the test is not a reliable combination. The way I do it is spying on certain component or subscribing to changes in the store in order to call “done()”, as explained in previous posts. But if you call “done()” before the expectation, it finishes and no expectation is executed. On the other hand, if the expectation fails “done()” is never called and the test fails with a timeout.

This is a helper function to work around the issue:

export function eventuallyExpect(expectation, done) {
return function () {
try {
expectation();
if (done) { done();}
}
catch (err) {
if (done) {done.fail(err);}
}
};
}

where expectation is a callback. That could be used like this:

it ('populates store with data returned from server', (done) => {
stubApiClientToReturn({username: 'carlos'});
simulateSearchFormSubmit({username: 'car'});
expectStoredUsersToContainUsername('carlos', done);
});
function expectStoredUsersToContainUsername(username, done){
store.subscribe(eventuallyExpect(() => {
expect(store.getState().users[0].username).to.equal(username);
}, done));
}
view raw tests.js hosted with ❤ by GitHub

Things start to get messy if the store is updated several times during that test, as the listener will be invoked when the data is not in the expected state yet. When that happens is time to re-think the design of the system and strive for simpler options.

Sometimes asynchronous tests fail with a timeout because there is some unhandled promise rejection. The console prints out this warning:

 (node:9040) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 97): Error: ...

It’s possible and very useful to configure node so that when rejection happens the execution stops printing the stack trace:

function stopOnUnhandledPromise() {
process.on('unhandledRejection', (reason) => {
console.error(reason);
process.exit(1);
});
}

Invoke the function before running the tests.

Apart from asynchronous tests, other issue we had was testing changes in the URL, in the browser history. We have a component that changes the query string when submitting a form. It does not post the form. The same component parses the URL every time it changes or when it loads looking for changes in the query string in order to make requests to the server. The query string serves as a filter. After spending sometime, I didn’t know how to simulate changes in the location with jsdom. All I got were security exceptions so changing the browser history was not a working option unfortunately.

To work around this, given we are using react-router-redux and enzyme’s mount, we connect a stub router to the component explicitly as shown below. Basically react-router-redux connects the router with the component for you by adding a “routing” property to the component that maps to “state.routing” thus causing component’s _ “componentWillReceiveProps”_ to be executed as the router handles a change in the location. In the tests, I have to make this mapping explicitly because the router does not work with jsdom as far as I have researched. The code below will be part of the test file:

let stubApi = {};
let store;
let stubRouter = {
location: {
search: '',
query: {
toDay: '',
fromDay: ''
}
},
push: () => {};
};
function mountPageSimulatingNextQueryString(queryString) {
let simulatedLocation = {
location: {search: queryString}
};
const connectRouterToPageExplicitly = (state) => {
return {
routing: state.routing
};
};
const Page = factory.createPage(stubApi, const connectRouterToPageExplicitly);
page = mount(
<Provider store={store}>
<Page router={stubRouter} {...simulatedLocation}/>
</Provider>);
}
function dispatchLocationChange(store){
// Triggers the action that ends up with a call to
// componentWillReceiveProps with the new location
// Warning: This will break if future versions of react-router
// change the string literal for type of the action.
store.dispatch({
type: '@@router/LOCATION_CHANGE',
payload: {}
});
}
it('requests the report to the server when url changes', (done) => {
store = configureStore();
let sentFilters = null;
stubApi.getTotalsReport = (filters) => {
sentFilters = filters;
return Promise.resolve({});
};
mountPageSimulatingNextQueryString(currentQueryString());
dispatchLocationChange(store);
onceArequestToServerHasBeenProcessed(() => {
expect(sentFilters).toEqual(currentQueryString());
}, done);
});
view raw tests.js hosted with ❤ by GitHub

The factory - production code:

export const createPage = (serverApi, mapState) => {
return connect(
(state) => {
let mapping = {
report: state.report
};
if (typeof(mapState) === 'function'){
return Object.assign(mapping, mapState(state));
}
return mapping;
},
(dispatch) => ({
// irrelevant for this post
})
)(Page);
};
view raw factory.js hosted with ❤ by GitHub

It’s quite tricky to be honest but it exercises the production code exactly as running the application manually. The code snippet in the test file is not complete as you can imagine, only the relevant lines appear.

Another difficulty we run into recently was simulating a scroll event. At the time of this writing, apparently jsdom does not implement a way to simulate scrolling so calling simulate(‘scroll’) does nothing. To work around this, I invoke the event handler directly as it’s a public method on the component. Well, it’s public because ES6 classes don’t implement private methods. Instance methods can be accessed through enzyme’s _“instance()”, method on the wrapper object. However this method works only on the parent component. If you want to access a child component it gets more tricky. It could be the case of a component that should be child of a draggable in order for drag & drop to work:

it('...', () => {
const offset = 1000;
wrapper = mount(
<TestableDragableComponent>
<ComponentUnderTest />
</TestableDragableComponent>, mountContext);
const componentUnderTest = wrapper
.find(ComponentUnderTest)
.first().node;
componentUnderTest.scrollHandler(offset); // public method
expect...
}

The instance of the component under test is the first node.

Volver a posts