Tests Strategies for React and Redux
When the Firefox Add-ons team ported addons. mozilla. org to a single page application backed by a good API , we chose React and Redux for effective state management , delightful developer tools , and testability. Achieving the testability component isn’ t completely obvious classes competing tools and techniques.
Below are some testing techniques that are working really well for us.
Testing must be fast plus effective
We want the tests to be lightning fast to ensure that we can ship high-quality features rapidly and without discouragement. Waiting for tests could be discouraging, yet tests are crucial just for preventing regressions, especially while restructuring an application to support new features.
Our strategy is to just test what’ s necessary in support of test it once. To achieve this we check each unit in isolation, faking away its dependencies. This is a technique referred to as unit examining and in our situation, the unit is typically a single React component .
A suite associated with unit tests combined with static kind analysis is very fast and efficient. We use Jest because it as well is fast, and because it allows us to focus on a subset of checks when needed.
Testing Redux connected components
The risks of testing in isolation in just a dynamic language are not entirely relieved by static types, especially given that third-party libraries often do not deliver with type definitions (creating all of them from scratch is cumbersome). Also, Redux-connected components are hard to isolate simply because they depend on Redux functionality to keep their particular properties in sync with condition. We settled on a strategy exactly where we trigger all state adjustments with a real Redux store . Redux is vital to how our application operates in the real world so this makes our own tests very effective.
Because it turns out, testing with a real Redux store is fast. The design associated with Redux lends itself very well in order to testing due to how actions, reducers, and state are decoupled from one another . The tests give the right opinions as we make changes to app state. This also makes it feel like a great fit for testing. Aside from screening, the Redux architecture is great for debugging, scaling, and especially development .
Think about this connected component as an example: (For brevity, the examples in this article do not establish Flow types but you can learn about the right way to do that right here . )
import connect from 'react-redux'; import compose from 'redux'; // Define a functional React component. foreign trade function UserProfileBase(props) return ( <span>props.user.name</span> ); // Establish a function to map Redux state to properties. function mapStateToProps(state, ownProps) return user: state.users [ownProps.userId] ; // Export the last UserProfile component composed of // a situation mapper function. export default compose( connect(mapStateToProps), )(UserProfileBase);
You may be tempted to test this simply by passing in a synthesized user home but that would bypass Redux and all sorts of your condition mapping logic. Rather, we test by dispatching a genuine action to load the user into condition and make assertions about what the particular connected component rendered.
import mount through 'enzyme'; import UserProfile from 'src/UserProfile'; describe('< UserProfile> ', () => it('renders a name', () => const store = createNormalReduxStore(); // Simulate fetching an user from an API and loading it into state. store.dispatch(actions.loadUser( userId: 1, name: 'Kumar' )); // Render with an user ID so it can retrieve the user from state. const root = mount(<UserProfile userId=1 store=store />); expect(root.find('span')).toEqual('Kumar'); ); );
Rendering the full component with Enzyme’ s
mapStateToProps() is working and that the reducer did what this specific component anticipated. It simulates what would happen when the real application requested an user through the API and dispatched the result. Nevertheless , since
mount() renders all components which includes nested components, it doesn’ big t allow us to test
UserProfile in remoteness. For that we need a different approach making use of shallow making , explained below.
Shallow rendering for dependency shot
Let’ s state the
UserProfile component depends on a
UserAvatar aspect of display the user’ s picture. It might look like this:
export function UserProfileBase(props) const user = props; return ( <div> <UserAvatar url=user.avatarURL /> <span>user.name</span> </div> );
UserAvatar will have unit tests of its personal, the
UserProfile test just has to ensure it calls the interface associated with
UserAvatar correctly. What is its interface? The particular interface to any React component is just its qualities . Flow helps to validate residence data types but we in addition need tests to check the data values.
With Enzyme, we don’ t have to substitute dependencies with reproductions in a traditional dependency injection feeling. We can simply infer their lifestyle through superficial rendering . A test would appear something like this:
import UserProfile, UserProfileBase through 'src/UserProfile'; import UserAvatar from 'src/UserAvatar'; import shallowUntilTarget from '. /helpers'; describe('< UserProfile> ', () => it('renders an UserAvatar', () => const user = userId: 1, avatarURL: 'https://cdn/image.png', ; store.dispatch(actions.loadUser(user)); const root = shallowUntilTarget( <UserProfile userId=1 store=store />, UserProfileBase ); expect(root.find(UserAvatar).prop('url')) .toEqual(user.avatarURL); ); );
Instead of calling
mount() , this test renders the particular component using a custom helper known as
shallowUntilTarget() . You might already be familiar with Enzyme’ s
shallow() but that just renders the first component in a shrub. We needed to create a helper known as
shallowUntilTarget() that will render all “ wrapper” (or increased order ) components till reaching our target,
Ideally Enzyme will ship a feature just like
shallowUntilTarget() soon, but the implementation is simple. This calls
root. dive() in a loop till
root. is(TargetComponent) returns true.
With this shallow rendering approach, it is currently possible to test
UserProfile in isolation though dispatch Redux actions like a genuine application.
The test searches for the
UserAvatar component in the tree and just makes sure
UserAvatar will receive the correct qualities (the
render() function of
UserAvatar is certainly not executed). If the properties of
UserAvatar modify and we forget to update the test, test might still pass, but Movement will alert us about the breach.
The elegance associated with both React and shallow making just gave us dependency shot for free, without having to inject any dependencies! The key to this testing strategy would be that the implementation of
UserAvatar is free to develop on its own in a way that won’ t crack the
UserProfile tests. If changing the particular implementation of an unit forces you to definitely fix a bunch of unrelated tests, it’ s a sign that your testing technique may need rethinking.
Crafting with children, not properties
The power of React plus shallow rendering really come into concentrate when you create components using kids instead of passing JSX via attributes. For example , let’ s say a person wanted to wrap
UserAvatar in a common
InfoCard regarding layout purposes. Here’ s the way to compose them together as kids:
foreign trade function UserProfileBase(props) const user = props; return ( <div> <InfoCard> <UserAvatar url=user.avatarURL /> </InfoCard> <span>user.name</span> </div> );
After making this change, exactly the same assertion from above will still function! Here it is again:
expect(root. find(UserAvatar). prop('url')) . toEqual(user. avatarURL);
In some cases, you may be tempted to pass JSX through properties instead of through kids. However , common Enzyme selectors such as
root. find(UserAvatar) would no longer work. Let’ s look at an example of passing
UserAvatar in order to
InfoCard through a
export function UserProfileBase(props) const user = props; const avatar = <UserAvatar url=user.avatarURL />; return ( <div> <InfoCard content=avatar /> <span>user.name</span> </div> );
This is nevertheless a valid implementation but it’ h not as easy to test.
Testing JSX passed through properties
Sometimes you really can’ big t avoid passing JSX through qualities. Let’ s imagine that
InfoCard needs complete control over rendering some header content material.
foreign trade function UserProfileBase(props) const user = props; return ( <div> <InfoCard header=<Localized>Avatar</Localized>> <UserAvatar url=user.avatarURL /> </InfoCard> <span>user.name</span> </div> );
How would you test this particular? You might be tempted to do a full Chemical
mount() as opposed to the
shallow() render. You might think it will provide you with much better test coverage but that extra coverage is not necessary — the particular
InfoCard component will already have tests from the own. The
UserProfile test just has to make sure
InfoCard gets the right properties. Here’ s how to test that.
import shallow from 'enzyme'; import InfoCard through 'src/InfoCard'; import Localized from 'src/Localized'; import shallowUntilTarget from '. /helpers'; describe('< UserProfile> ', () => it('renders an InfoCard with a custom header', () => const user = userId: 1, avatarURL: 'https://cdn/image.png', ; store.dispatch(actions.loadUser(user)); const root = shallowUntilTarget( <UserProfile userId=1 store=store />, UserProfileBase ); const infoCard = root.find(InfoCard); // Simulate how InfoCard will render the // header property we passed to it. const header = shallow( <div>infoCard.prop('header')</div> ); // Now you can make assertions about the content: expect(header.find(Localized).text()).toEqual('Avatar'); ); );
This is better than a full
mount() because it enables the
InfoCard implementation to evolve openly so long as its properties don’ to change.
Testing element callbacks
Aside from transferring JSX through properties, it’ t also common to pass callbacks in order to React components. Callback properties allow it to be very easy to build abstractions around typical functionality. Let’ s imagine we have been using a
FormOverlay component to render an modify form in a
import FormOverlay through 'src/FormOverlay'; export class UserProfileManagerBase expands React. Component onSubmit = () => // Pretend that the inputs are controlled form elements and // their values have already been connected to this.state. this.props.dispatch(actions.updateUser(this.state)); render() return ( <FormOverlay onSubmit=this.onSubmit> <input id="nameInput" name="name" /> </FormOverlay> ); // Foreign trade the final UserProfileManager component. export arrears compose( // Use connect() through react-redux to get props. dispatch() connect(), )(UserProfileManagerBase);
How can you test the integration of
UserProfileManager along with
FormOverlay ? You could be tempted once again to do a full
mount() , especially if you’ re examining integration with a third-party component, something similar to Autosuggest . However , a full
mount() is not necessary.
Just like in previous illustrations, the
UserProfileManager test can simply check the attributes passed to
FormOverlay . This is safe because
FormOverlay may have tests of its own and Stream will validate the properties. The following is an example of testing the
import FormOverlay from 'src/FormOverlay'; import shallowUntilTarget through '. /helpers'; describe('< UserProfileManager> ', () => it('updates user information', () => const store = createNormalReduxStore(); // Create a spy of the dispatch() method for test assertions. const dispatchSpy = sinon.spy(store, 'dispatch'); const root = shallowUntilTarget( <UserProfileManager store=store />, UserProfileManagerBase ); // Simulate typing text into the name input. const name = 'Faye'; const changeEvent = target: name: 'name', value: name , ; root.find('#nameInput').simulate('change', changeEvent); const formOverlay = root.find(FormOverlay); // Simulate how FormOverlay will invoke the onSubmit property. const onSubmit = formOverlay.prop('onSubmit'); onSubmit(); // Make sure onSubmit dispatched the correct ation. const expectedAction = actions.updateUser( name ); sinon.assertCalledWith(dispatchSpy, expectedAction); ); );
This tests the particular integration of
FormOverlay without counting on the implementation of
FormOverlay . It uses sinon in order to spy on the
shop. dispatch() method to guarantee the correct action is dispatched once the user invokes
Every modify starts with a Redux action
The Redux architecture is straightforward: when you want to change application state, give an action. In the last example of examining the
onSubmit() callback, the test simply true a dispatch of
actions. updateUser(... ) . That’ s it. This test presumes that once the
updateUser() action is sent, everything will fall into place.
So how would an application such as ours actually update the user? We might connect a saga to the activity type. The
updateUser() saga would be accountable for making a request to the API and dispatching further actions when receiving a reaction. The saga itself will have device tests of its own. Since the
UserProfileManager check runs without any sagas, we don’ t have to worry about mocking out the particular saga functionality. This architecture can make testing very easy; something like redux-thunk might offer similar benefits.
These good examples illustrate patterns that work really well in addons. mozilla. org for resolving common testing problems. Here is a summarize of the concepts:
- We dispatch real Redux activities to test application state changes.
- We test each element only once using shallow rendering.
- We resist full DEM rendering (with
mount()) as much as possible.
- We test component integration simply by checking properties.
- Stationary typing helps validate our element properties.
- We imitate user events and make statements about what action was dispatched.
Want to get more involved with Firefox Add-ons community? There are a web host of ways to contribute to the addons ecosystem – plus plenty to learn, whatever your abilities and level of experience.
Kumar hacks on Mozilla web solutions and tools for various tasks, such as those supporting Firefox Add-ons . He or she hacks on lots of random open source tasks too.
If you liked Tests Strategies for React and Redux by Then you'll love Web Design Agency Miami