Jest unit testing for React.js
It’s common knowledge at this point that unit tests have a variety of benefits. In this post I’ll be describing my trials and triumphs implementing Jest unit testing for React.js. Three aspects I grappled with while developing Jest-based testing were customizable defaults, manual mocking, and testing interaction with the document window.
Customizable defaults
Every React component has a set of props that are commonly passed down from parents to children. There are instances when your props may change later due to new data or data needed conditionally, so it’s advantageous that you can also declare new unknown props that may come later using the JSX spread operator. It’s reasonable to assume, when writing tests, that not only may you want to add props, but also change some of the defaults to assert a different outcome.
The question is, how can we provide default props to our component tests and also be able to supply differing prop values in a comprehensible way? Our example test will attempt to solve this by building a fresh component for each segment of the test using a factory pattern method and underscore defaults. The component will be given default props that can be overwritten at the time of object creation. The benefit of _.defaults is it will fill in undefined properties in the object as well as use the first value present in the following list of defaults objects. This means we can feed in different props to overwrite a property later to assert a different outcome, but otherwise the default props will be used.
The creation method:
jest.dontMock('../component');const MyComponent = require('../component');
window._ = require("underscore");const createComponent = (props = {}) => {
const defaultProps = {
onClick: jest.genMockFunction(),
assets: 1
};const ComponentProps = _.defaults(props, defaultProps);return TestUtils.renderIntoDocument(<MyComponent {...ComponentProps} />);
};describe('Building the component with props', () => {
it('builds the component with one asset ', () => {
let component = createComponent();
// We create our component with default props (assets: 1) so we'll expect one object
expect(component.props.children.length).toBe(1);
});it('builds the component with three asset ', () => {
let component = createComponent({assets: 3});
// We create our component with new props (assets: 3) so we'll expect three objects
expect(component.props.children.length).toBe(3);
});
});Manual mockingOne of the core concepts of using Jest is mocking. Jest will automatically mock everything unless you explicitly tell it otherwise. As seen in the example above, in order to test using the real component, you'll need to tell Jest to not mock your component and then require it. Also take note that to use an external library like underscore we'll need to require it (in this procedure) and associate it with the _ character on the window.This is all well and good, but what if you need more customizable mocking, say for an internal API? Our app uses a top-level configuration object that exposes the API of a related micro-service. This API returns information about the internal state of the micro-service, such as the mode your app is currently running in or some data formatted in an explicit way. Automatic mocking won't cut it here as our program is expecting nested objects that return a specific data format. The answer to this quandary is manual mocking. If you dig a bit in the documentation you'll find that just as Jest will automatically find your __tests__ directory, It'll do the very same with an appropriately located __mocks__ folder.Top-level configuration mock object:let Configuration = {
api: {
exampleData: jest.genMockFunction().mockReturnValue(['example 1', 'example 2'])
},
whereAmI: 'Space'
};module.exports = Configuration;
Now we have a mocked version of our internal configuration API that when called upon within our component will return an expected response. But what if we need to augment this mocked file in our test to expect different outcomes like we did with our props? It wasn't immediately clear, but the solution here is to require the manually mocked version of the file in your test. From this point you can simply change the values of the object within the scope of your block at your own discretion.// Require the mock file
const Configuration = require('../../lib/__mocks__/configuration');describe('Tells the component where we are', () => {
let component = createComponent();
it('builds the component and tells the component where it is ', () => {
// overwrite the configuration value
Configuration.whereAmI = 'Earth'
let renderedComponent = TestUtils.findRenderedDOMComponentWithClass(component, 'component--is-rendered');
expect(renderedComponent.getDOMNode().textContent).toBe('We are on Earth');
});
});Interaction with the document windowThe goal in React is to write 'pure components', but sometimes in practice your code can't be so tidy. In one area of our code a component has to listen for a click on the document window. Jest doesn't really care about what's happening outside of React, providing a way to simulate a click event on a component, but not for the document window. Inserting the document as a parameter for ReactTestUtils would have been a nice addition for this common interaction, but this is unavailable. Nevertheless, there is a work-around for this situation. We can manually create an event and listen for it before making an assertion about the change in component state.Manual creation of a document click event:expect(component.state.documentWasClicked).toBe(false);
var evt = new Event("click", {"bubbles": false, "cancelable": true});
document.addEventListener('click', component.onDocumentClick);
document.dispatchEvent(evt);
// Our component state has responded to our manually created click event
expect(component.state.documentWasClicked).toBe(true);
Conclusion
I found Jest provided a very helpful way to test React components, with the ability to easily use your actual components and render them into a mocked DOM. That being said, the gritty reality of a code base can present unique challenges that Jest is not well-equipped to handle. Shifting parameters that change output, mocked objects for tests that depend upon specific data structures, and code that interacts with the document window all add complexity to Jest unit tests. Hurdles aside, with some effort you can customize, mock, and simulate almost anything you need. This technology gives a feasible and friendly route to test the front-end of your React application.Special thanks to Becca Liss and Joe Natalzia for helping me at various stages.