React components appear incredibly simple to test at first glance. After all, they're just functions of their state and props, right?
function MyComponent(props) {
return (
<div>
I have worked for {props.company} for {props.years} years.
</div>
);
}
const wrapper = mount(<MyComponent company="TrialSpark" years={2} />);
expect(wrapper.text()).toBe('I have worked at TrialSpark for 2 years.');
wrapper.setProps({ company: 'Facebook', years: 0 });
expect(wrapper.text()).toBe('I have worked at Facebook for 0 years.');
Wrong! And there's a single react feature we can blame for this: context. Context allows components to provide data that all its ancestors access without passing that data down as props. In practice, this is a helpful feature, and it powers some of the most popular libraries out there today: react-redux
, react-router
, react-apollo
, to just name a few.
However, it creates some real chalenges when we go to test our components:
let MyComponent = function MyComponent(props) {
return <div>My name is {props.name}.</div>;
};
const mapStateToProps = state => ({
name: state.user.name,
});
MyComponent = connect(mapStateToProps)(MyComponent);
// Error: store missing in context
const wrapper = mount(<MyComponent />);
One common way to solve this problem is to render your component inside of a provider:
import { Provider } from 'react-redux';
import store from './myStore';
const wrapper = mount(
<Provider store={store}>
<MyComponent />
</Provider>,
);
However, this doesn't play very nicely with enzyme:
wrapper.type(); // Provider, not MyComponent
wrapper.setProps({}); // Setting props on the Provider, not MyComponent
wrapper.instance(); // Instance of Provider, not MyComponent
Thankfully, enzyme
allows us to pass context to our components when we mount this:
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import store from './myStore';
const wrapper = mount(<MyComponent />, {
context: {
store,
},
childContextTypes: {
store: PropTypes.any,
},
});
wrapper.type(); // MyComponent
While the above solution works, its imperfect for a couple of reasons:
Contexts are usually global to our app. For example, an app typically has one store, one router, etc. However, passing context to enzyme requires us to setup our global context every time we mount a component. This leads to a lot of boilerplate when writing tests, and allows for the possibility of not bootstrapping context correctly, making our tests potentially less effective. This becomes more of a problem as we add more libraries that use context to our app.
Libraries like react-redux
do a good job of shielding us from context. It's API consists entirely of components: just wrap your whole app in a <Provider />
and you're off to the races! However, to pass context to enzyme correctly, we need to know about the implementation of the react-redux
provider—what context does it provide to its children?
React-redux
is pretty simple. It just passes a store
as context. Other libraries are more complicated. And, like before, the more libraries we add, the more internals we must know about.
Enzyme Context solves this problem by allowing you to create a custom mount
/shallow
function for your app that has all of your app's context set up for you already! It is built on top of a plugin system so you can add as many plugins as are necessary for the libraries in your app. This way, the plugins can worry about the interals of the libraries they are supporting, and you can focus on testing your application.