Skip to content

Instantly share code, notes, and snippets.

@joshdover
Last active September 28, 2023 21:38
Show Gist options
  • Save joshdover/235714771d94509a83609b16d232014a to your computer and use it in GitHub Desktop.
Save joshdover/235714771d94509a83609b16d232014a to your computer and use it in GitHub Desktop.
Idiomatic React Testing Patterns

Idiomatic React Testing Patterns

Testing React components seems simple at first. Then you need to test something that isn't a pure interaction and things seem to break down. These 4 patterns should help you write readable, flexible tests for the type of component you are testing.

Setup

I recommend doing all setup in the most functional way possible. If you can avoid it, don't set variables in a beforeEach. This will help ensure tests are isolated and make things a bit easier to reason about. I use a pattern that gives great defaults for each test example but allows every example to override props when needed:

const getComponent = (props = {}) => {
	// Any test can override the default props by passing an object to the getComponent function
	props = Object.assign({
		onChange: sinon.spy(),
		title: 'Test Title',
		color: 'red'
	}, props);

	const component = ReactDOM.findDOMNode(TestUtils.renderIntoDocument(
		<MyComponent {...props} />
	));

	return Object.assign(props, { component });
};

// Usage
const { component, onChange } = getComponent();
const { component } = getComponent({ onChange: someFunc });

The Patterns

There are 4 primary patterns that I've identified, have more ideas? Provide examples and rationale in the comments!

Preferred "pure" patterns

Impure patterns

Inevitably you will have impure components, these patterns provide consistency in how you test these types of interactions

  • Stateful interactions: how interactions change render results relying on this.state or how lifecycle hooks behave
  • Instance methods: how the component interacts with external libraries or APIs (very rare)

Disclaimer

You may have a different testing stack and YMMV with how well these patterns work within that environment. For reference, here is the stack we use at Cratejoy and the one I've had the most success with:

  • Karma test runner (configured with browserify + babel)
  • Mocha test framework
  • Chai expect library
  • Sinon mock library
import { expect } from 'chai';
import sinon from 'sinon';
import React from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-addons-test-utils';
import MyComponent from './MyComponent';
/*
* Pattern to be used for testing instance methods of components. These should not be used to test impementation details.
* Examples of good applications of this pattern:
* - Testing interactions with a stateful DOM API (eg. iframe). NOTE: components should not interact with DOM APIs
* that are not related to visual display.
* - Test interactions with an external UI library (eg. an image editor like Aviary)
*/
describe('MyComponent', () => {
const getComponent = (props = {}) => {
props = Object.assign({
onChange: sinon.spy(),
}, props);
const node = document.createElement('div');
// Notice the different rendering method here
const component = ReactDOM.render(
<MyComponent {...props} />
), node);
return Object.assign(props, { component });
};
describe('myMethod', () => {
it('returns some value', () => {
const { component } = getComponent();
expect(component.myMethod()).to.equal('some value');
});
});
});
import { expect } from 'chai';
import sinon from 'sinon';
import React from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-addons-test-utils';
import MyComponent from './MyComponent';
/*
* Pattern to be used to assert basic rendering expectations, including how props change the output.
*/
describe('MyComponent', () => {
const getComponent = (props = {}) => {
// Any test can override the default props by passing an object to the getComponent function
props = Object.assign({
onChange: sinon.spy(),
}, props);
const component = ReactDOM.findDOMNode(TestUtils.renderIntoDocument(
<MyComponent {...props} />
));
return Object.assign(props, { component });
};
it('renders a h1 for title prop', () => {
const { component } = getComponent({ title: 'My Label' });
expect(component.querySelector('h1').innerText).to.equal('My Label');
});
});
import { expect } from 'chai';
import sinon from 'sinon';
import React from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-addons-test-utils';
import MyComponent from './MyComponent';
/*
* To be used when testing a component that is _NOT_ pure (uses this.state) OR when testing lifecycle hooks
* (eg. componentDidUpdate). This is accomplished by re-rendering the the component manually and then asserting
* expectations.
*/
describe('MyComponent', () => {
const getComponent = (props = {}) => {
props = Object.assign({
onChange: sinon.spy(),
}, props);
const node = document.createElement('div');
// Notice the different rendering method here
const component = ReactDOM.render(
<MyComponent {...props} />
), node);
return Object.assign(props, { component, node });
};
context('when clicked', () => {
it('adds some-class', () => {
const props = getComponent();
// Do some action that changes internal state
TestUtils.Simulate.click(props.component);
// Re-render (you can also change props here)
ReactDOM.render(<MyComponent {...props} />, props.node);
expect(component.className).includes('some-class');
});
});
});
import { expect } from 'chai';
import sinon from 'sinon';
import React from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-addons-test-utils';
import MyComponent from './MyComponent';
/*
* Pattern to be used to assert pure interaction expectations that do not require any lifecycle hooks or internal state.
*/
describe('MyComponent', () => {
const getComponent = (props = {}) => {
props = Object.assign({
onChange: sinon.spy(),
}, props);
const component = ReactDOM.findDOMNode(TestUtils.renderIntoDocument(
<MyComponent {...props} />
));
return Object.assign(props, { component });
};
context('when the component is changed', () => {
it('calls onChange', () => {
const { component, onChange } = getComponent();
const inputNode = input.querySelector('input[type=text]');
TestUtils.Simulate.change(inputNode, { target: { value: 'new' } });
expect(onChange.calledWith('new')).to.be.true;
});
});
});
@justsml
Copy link

justsml commented Jul 14, 2016

Excellent work @joshdover

@snahor
Copy link

snahor commented Aug 12, 2016

🎉 I would have loved to find this some months ago, I had to learn this the hard way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment