My work on a recent project has followed a very by-the-book Test-Driven Development (TDD) approach. I’ve become a huge fan and promoter of the practice, which has led me to examine and identify strategies to avoid interdependence, support common set-up and teardown logic, and bypass mucking about with implementation details.
What follows is a comprehensive set of best practices I’ve identified for circumspect React/Redux Unit Testing. We’ll cover:
This article assumes you are already happy and comfortable using React, Redux, and know the basic testing methods from Enzyme.
All we care about here is that the correct action creator was called and it returned the right action. So unit tests should only know about actions/events and state.
For async action creators using Redux Thunk (or other middleware), mock the (minimally) required Redux store for testing. If needed, you can apply the middleware to said store using redux-mock-store.
You can also use fetch-mock to mock the HTTP requests, but that might be overkill. I’ve found it preferable to simply use a mockServiceCreator function with a suite of body fixtures.
Here’s a somewhat complicated async action creator we would like to test that does one or two factor authentication:
The entire reducer might look something like:
And here is the base of the testing suite for the entire authentication flow, using redux-mock-store and thunks:
The above action creator is tested thus (with the body response an external imported ‘fixture’):
A reducer should just return a new state object after applying an action to the previous state.
So for the reducer I introduced above, we can simply do:
What does the component render? What props does it receive? What state does it hold? How do these things change during user interaction? Is the rendering conditional depending on these aspects?
Shallow render whenever possible!
Test to make sure all sub-components of the component you are testing at least render (using manually passed, logic-dictating props as needed).
Test the component when not connected to Redux. Directly export your unconnected component alongside default export of the connected component. E.g.:
It is not necessary to test that our mapDispatchToProps is properly passing a function (login, submit, click etc.) to the connected component, because Redux is already responsible for this.
If you want to test its interaction with Redux, you can wrap it in a with a store created specifically for this unit test. (But this seems more like Integration/Journey testing.)
When we simulate a DOM event, we need to pass the preventDefault function to our event object.
This is because the function will call event.preventDefault(); if we don’t include it an error occurs. Once we simulate the submit event, we can then test our event handler with a mock function to see if it was called (purpose here isn’t to test React event wiring, or browser event handling). E.g.:
There’s no need to test that React will trigger a “change” event when a element is clicked (i.e. a checkbox)-just shallow-render your Component, and assert that appropriate attribute (i.e. onChange) matches the prop or state you pass in manually to your test. You should be using state to store the current state of a given element element and base your rendering on that. Remember how Eventing works in a proper React setup.
Test the component, not the connection.
Don’t test the rendered HTML.
Per above, we really should not be changing state/props through Eventing. Rather we can test that each form element event handler fires the appropriate prop function, and that it perhaps changes ‘state’ in the appropriate way. Then we can simulate the entire form getting submitted. Again, _Enzyme’s shallow( … ).simulate() does not bubble events; it gets the event handler and invokes it.
Of course, forms are only useful when filled, and there can be a myriad of different error handling methods and routings in result. The naive testing strategy would be to use mount several time to manipulate form fields to set a given state to test the results of submit against. Or maybe to simulate a change event of several fields. Instead, we should set these internals directly as we have already tested the event handlers of individual form elements. setState is available, but per the docs “This method is useful for testing your component in hard to achieve states, however should be used sparingly. If possible, you should utilize your component’s external API (which is accessible via .instance()) in order to get it into whatever state you want to test, in order to be as accurate of a test as possible. This is not always practical, however.”
So instead of something like:
If it isn’t clear, above we had something like the below as a method on the AddExternalAccount component:
And the component looked something like:
In other words, we should try to grab our form, and use the real state setting methods called by the event handlers to drive our form’s data for a given test.
Lastly, Enzyme makes it really easy to test a component like this but it was non-obvious how to test the default value of the form.
Here we can just shallow render the component, call find() on the test id and then use .node.value to get the default value.
The main idea is to put only the public API of a component under test and make assertions on the output.
Use redux-mock-store rather than your real redux store. Service (async calls) can be mocked easily using Promises and setTimeOut. Test Reducers as simple pure functions. Shallow render components. Test for the presence of the sub-components you expect. Avoid unit testing on HOCs (remember anything connected to Redux is a HOC). Don’t simulate events to test forms. Above all, don’t waste your time retesting React/Redux/DOM APIs.