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 .
Unfortunately, it’ s very hard to do this safely in a dynamic vocabulary such as JavaScript since there is no fast method to make sure the fake objects are in synchronize with real ones. To solve this particular, we rely on the safety associated with static typing (via Flow ) in order to alert us if one element is using another incorrectly — some thing an unit test might not capture.
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 mount()
ensures 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>
);
Considering that 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, UserProfileBase
.
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 content
property:
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 UserProfileManager
component.
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 onSubmit
property.
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 UserProfileManager
and 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 onSubmit()
.
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.
Summary
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