A lot of JavaScript applications out there require having a browser available to run your unit tests. For years it seemed like the de-facto configuration for unit testing was some combination of Karma, PhantomJS, and either QUnit or Jasmine. While I think that there is definitely value in making sure that your application runs properly in a browser, given that that is how your users will interact with it, my personal opinion is that the majority of your test suite should be able to be run outside of a browser environment. This article will describe what Enzyme brings to the table in terms of unit testing your React/Redux code without needing a browser.
The Application
This application is a very simple React/Redux application using a single reducer and several components to simply display the application name and a purpose, and then allow the user to update either. There are several distinct parts of the application and each should have their own unit tests.
The Reducer
Reducers are simply pure functions that take in the previous state and an action, and then returns what the next state is going to be based on the action data. Since they are pure functions they are going to be really easy to test and would not require anything fancy for your tests.
const root = (state = wrappedState, action) => { switch (action.type) { case 'UPDATE_NAME': { return state.set('appName', action.appName); } case 'UPDATE_PURPOSE': { return state.set('purpose', action.purpose); } default: { return state; } } };
The Action Creator
Action creators are also supposed to be simple functions that take in parameters and return a plain object that tells the reducer how to modify the state. If you use redux-thunk or redux-saga to allow you to make complicated asynchronous actions then your action creators might become more complicated to test. However, if you do have complicated asynchronous behaviors in your application (and what real world application does not), then you can create your own middleware that performs those actions when a specific action type is dispatched. Once that is done then your action creators become simple again, just return an action of the type that triggers that middleware along with whatever data is needed by the middleware. When your action creators are just simple functions that return simple objects then they become really easy to test just like your reducers.
export function updatePurpose(newPurpose) { return { type: 'UPDATE_PURPOSE', purpose: newPurpose, }; }
Selectors
If you are not using the reselect library to create your state querying logic, then I would strongly recommend that you start doing so. There are performance benefits to using it especially if you have derived data with expensive computations. If you do use reselect, then your selectors are just functions that take in the current state object and return a selection of data from it. Testing these is trivial since the same inputs will always return the same output.
const appNameFilter = (state) => state.get('appName'); const purposeFilter = (state) => state.get('purpose'); export const appInfoSelector = createSelector( appNameFilter, purposeFilter, (appName, purpose) => { return { appName, purpose }; } );
Containers
mapStateToProps
Since you are using Redux you will eventually have to connect some of your components to your Redux store to get data. Unless you are doing something really complicated this will just consist of two functions or objects plus your component that is to be connected to the store. Your “mapStateToProps” function should take in the current state as a parameter and then optionally the props that were passed to this container from its parent component. If you used reselect to create your selectors, then this function is where you would call those selectors and pass the current state to them. You would then compose the output from the calls to your various selectors into whatever prop shape is needed by your component. Since your “mapStateToProps” function is really only supposed to return a mapped subset of your state and passed props, this is also a simple piece to test.
export const mapStateToProps = (state) => { return appInfoSelector(state); };
mapDispatchToProps
If your component needs to trigger actions to update your Redux store then your “mapDispatchToProps” function is where props are created that contain references to your action creators. Testing this functionality is a little different because there are several ways you can approach it. You might say that you only care that certain props are passed in, so you might assert that a given prop exists and is a function. However, you might also want to assert that a given prop is a specific action creator. My preference is to usually assert the existence of a prop and that it is a function to protect myself from refactoring mistakes, but not to assert that it is a specific function reference.
import { updateAppName, updatePurpose } from '../modules/root'; export const mapDispatchToProps = { updateAppName, updatePurpose, };
Components
Now that we have looked at all of the non-UI portions of our app, let us consider the UI components themselves.
My example application has the following component structure.
App Header AppName Purpose EditForm
With this type of nested structure, if you were using something like PhantomJS to run your unit tests then you would likely be rendering the Header component into the DOM and running assertions based on what rendered. Since you would only see the final HTML representation and not the componentized representation, you would likely end up testing the functionality from your AppName and Purpose components as part of your Header component since they rendered as part of your test. This type of approach makes it really tough to test components in true isolation.
// Header export const Header = (props) => { return ( <header> <AppName appName={props.appName} /> <Purpose purpose={props.purpose} /> </header> ); }; // AppName export const AppName = (props) => { return ( <h1>{props.appName}</h1> ); }; // Purpose export const Purpose = (props) => { return ( {props.purpose} ); };
In this code example, if I write a test for the Header that does a full render then I might end up asserting that the application name was rendered in a tag. However, if somebody else comes along and changes the AppName component then that would break this Header test. Ideally what unit tests for the Header component should do would be to assert that it had a child component of type AppName and passed it the correct appName as a prop. What the AppName component does with that prop is beyond the scope of the Header component. The same thing holds for having a child component of type Purpose with the correct props passed into it. Once that is done then you would write separate unit tests for the AppName and Purpose components asserting that when given the appName or purpose they would render it properly.
Testing the Application
Mocha and Enzyme
There are several JavaScript libraries out there that make this sort of isolated UI unit testing possible. Mocha is a testing framework that I like but the library that really makes this sort of testing easy is Enzyme. Enzyme works with pretty much every testing framework or library out there so do not feel like you are locked in to Mocha if you want to use Enzyme. The important thing to note is that with Mocha you are most likely not going to be spinning up a web server which means you do not need Webpack. However, since Webpack is usually where developers put their transpilation steps, you may need to configure Mocha to perform any necessary transpilation so that your tests and code can be executed. This can be as simple as executing a file that requires in babel-register when Mocha is started.
// set up jsdom with some global window/document items require('jsdom-global')(); // use babel-register with the options from my .babelrc file require('babel-register');
Enzyme
Enzyme is described as a testing utility for React. The method that you will use if you want to perform isolated UI tests is the shallow render method. When you shallow render a component, Enzyme will build the component tree in memory, but it will stop at the first level of nested components. It will keep track of what props are passed in to which component though so you can perform assertions against those, but it will not actually render anything. Since it is not rendering anything then you do not need a browser or a DOM implementation, and since it is not building the component tree for nested components your tests will not break if the underlying implementations change. Another thing that is nice is that shallow rendering does not actually mount your component, so “componentDidMount” will not fire. Some components use that lifecycle method to trigger things like network requests which need to be mocked out in tests, so not having to do that in a shallow render is nice. If you do need to test the behavior of those lifecycle methods then the shallow wrapper around your component exposes an instance function that will return the actual component so that you can call methods directly against it in your testing.
That sounds nice from a unit testing perspective, but what if you want to write a few tests that make sure that the components work together properly? Enzyme does expose a render method which will perform a full render on your component. If you go with that approach though then you will need some type of DOM available for your tests, whether jsdom or an actual browser is up to you. My recommendation would be jsdom so that you do not need to spin up a browser and web server for your tests.
I would advise writing most of your tests as the pure unit test type, and only do the full render tests as needed to make sure everything is working together.
Really the only interesting test code that you need to see is how I tested the components. All of the other pieces of the app are tested just like plain JavaScript functions, although if you are interested then you can browse my code repo at https://github.com/jpeters5392/react-mocha-enzyme.
describe('Header', () => { it('should render an AppName', () => { const wrapper = shallow( <Header appName="MockAppName" purpose="MockPurpose" />); expect(wrapper.find(AppName)).to.have.length(1); }); it('should pass the appName prop to the AppName component', () => { const wrapper = shallow( <Header appName="MockAppName" purpose="MockPurpose" />); expect(wrapper.find(AppName).first().props().appName).to.equal("MockAppName"); }); it('should render a Purpose', () => { const wrapper = shallow( <Header appName="MockAppName" purpose="MockPurpose" />); expect(wrapper.find(Purpose)).to.have.length(1); }); it('should pass the purpose prop to the Purpose component', () => { const wrapper = shallow( <Header appName="MockAppName" purpose="MockPurpose" />); expect(wrapper.find(Purpose).first().props().purpose).to.equal("MockPurpose"); }); it('should not see the implementation of the Purpose component', () => { const wrapper = shallow( <Header appName="MockAppName" purpose="MockPurpose" />); expect(wrapper.find('p')).to.have.length(0); }); it('should see the implementation of the Purpose component if I render', () => { const wrapper = render( <Header appName="MockAppName" purpose="MockPurpose" />); expect(wrapper.find('p')).to.have.length(1); }); describe('mapStateToProps', () => { it('should set appName from the appName state value', () => { const state = Immutable.fromJS({ appName: 'MockAppName', purpose: 'MockPurpose', }); const props = mapStateToProps(state); expect(props.appName).to.equal("MockAppName"); }); it('should set purpose from the purpose state value', () => { const state = Immutable.fromJS({ appName: 'MockAppName', purpose: 'MockPurpose', }); const props = mapStateToProps(state); expect(props.purpose).to.equal("MockPurpose"); }); }); }); describe('AppName', () => { it('should render an h1', () => { const wrapper = shallow(<AppName appName="MockAppName" />); expect(wrapper.find('h1')).to.have.length(1); }); it('should render the app name inside of the h1', () => { const wrapper = shallow(<AppName appName="MockAppName" />); const h1 = wrapper.find('h1').first(); expect(h1.text()).to.equal("MockAppName"); }); }); describe('Purpose', () => { it('should render a p', () => { const wrapper = shallow(<Purpose purpose="MockPurpose" />); expect(wrapper.find('p')).to.have.length(1); }); it('should render the purpose inside of the p', () => { const wrapper = shallow(<Purpose purpose="MockPurpose" />); const p = wrapper.find('p').first(); expect(p.text()).to.equal("MockPurpose"); }); }); describe('EditForm', () => { it('should render a div with class of update-app-name', () => { const wrapper = shallow(<EditForm />); expect(wrapper.find('div.update-app-name')).to.have.length(1); }); it('should render a div with class of update-app-purpose', () => { const wrapper = shallow(<EditForm />); expect(wrapper.find('div.update-app-purpose')).to.have.length(1); }); it('should have a button inside of the update-app-name div', () => { const wrapper = shallow(<EditForm />); expect(wrapper.find('div.update-app-name button')).to.have.length(1); }); it('should have a button inside of the update-app-purpose div', () => { const wrapper = shallow(<EditForm />); expect(wrapper.find('div.update-app-purpose button')).to.have.length(1); }); it('should have a text input inside of the update-app-name div', () => { const wrapper = shallow(<EditForm />); expect(wrapper.find('div.update-app-name input[type="text"]')).to.have.length(1); }); it('should have a text input inside of the update-app-purpose div', () => { const wrapper = shallow(<EditForm />); expect(wrapper.find('div.update-app-purpose input[type="text"]')).to.have.length(1); }); it('should call props.updateAppName with the input value when clicking on the update app name button', () => { const spy = sinon.spy(); const wrapper = shallow(<EditForm updateAppName={spy} />); const instance = wrapper.instance(); instance.appName = { value: 'new app name', }; const button = wrapper.find('div.update-app-name button'); button.props().onClick(); expect(spy.calledOnce).to.be.true; expect(spy.getCall(0).args).to.eql(['new app name']); }); it('should call props.updatePurpose with the input value when clicking on the update app purpose button', () => { const spy = sinon.spy(); const wrapper = shallow(<EditForm updatePurpose={spy} />); const instance = wrapper.instance(); instance.purpose = { value: 'new app purpose', }; const button = wrapper.find('div.update-app-purpose button'); button.props().onClick(); expect(spy.calledOnce).to.be.true; expect(spy.getCall(0).args).to.eql(['new app purpose']); }); describe('mapDispatchToProps', () => { it('should set a prop for updating the app name', () => { expect(mapDispatchToProps.updateAppName).to.be.a('function'); }); it('should set purpose from the purpose state value', () => { expect(mapDispatchToProps.updatePurpose).to.be.a('function'); }); }); });
Conclusion
I hope that this post has given you some ideas of how you might be able to write pure unit tests even in your UI layer by leveraging Enzyme’s shallow rendering. I will pretty much guarantee that your unit tests will run faster and be more stable once you start running them without a browser or web server, and if you leverage shallow rendering then your testing layers become much simpler as well. The full source code for this sample app, including the build and test steps, is available at https://github.com/jpeters5392/react-mocha-enzyme.
Leave a Comment