Testing ⚛️ components using render props

January 8th, 2018 — 7 min read

by Scott Walsh
by Scott Walsh
No translations available.Add translation

With the release of my Advanced React Component Patterns course on egghead.io, a lot of people have been asking me about render props. Specifically with regards to testing. Maybe eventually I'll create a course on egghead.io about testing react components. Until then, I've decided to write this about some approaches that could help you when testing a component that renders a render prop component :)

Note: This isn't about how to test components that implement the render prop pattern. Rather, this is about how to tests components that_ use _components that implement the render prop pattern :)

In preparing this blog post, I created this repo which totally works and you can give it a look if you want more details :) In that repo, we have a component called FruitAutocomplete which is (basically) implemented like so:

import * as React from 'react'
import { render } from 'react-dom'
import Downshift from 'downshift'

const items = ['apple', 'pear', 'orange', 'grape', 'banana']

function FruitAutocomplete({ onChange }) {
	return (
		<Downshift
			onChange={onChange}
			render={({
				getInputProps,
				getItemProps,
				getLabelProps,
				isOpen,
				inputValue,
				highlightedIndex,
				selectedItem,
			}) => (
				<div>
					<label {...getLabelProps()}>Enter a fruit</label>
					<input {...getInputProps()} />
					{isOpen ? (
						<div data-test="menu">
							{items
								.filter((i) => !inputValue || i.includes(inputValue))
								.map((item, index) => (
									<div
										{...getItemProps({
											key: item,
											'data-test': `item-${item}`,
											index,
											item,
											style: {
												backgroundColor:
													highlightedIndex === index ? 'lightgray' : 'white',
												fontWeight: selectedItem === item ? 'bold' : 'normal',
											},
										})}
									>
										{item}
									</div>
								))}
						</div>
					) : null}
				</div>
			)}
		/>
	)
}

export default FruitAutocomplete

End to End tests

First off, I should say that render props are really just an implementation detail. So if you're writing E2E tests (with something like the amazing Cypress.io), then you shouldn't have to test anything any differently whether you're using render props or anything else. You just interact with the component the way the user would (type in the input, select an item, etc.). That may be obvious, but I think that brings up a pretty important point. The higher you're up on the "testing pyramid," the less implementation details matter, as you go down the pyramid, you have to deal with implementation details a little more.

UI, Service, Unit

Integration Tests

That said, I suggest focusing on integration tests. With an integration test, you likewise don't have to change too much about how you test the component. Here are the integration tests from the repo. You'll notice that there's no indication that the FruitAutocomplete component is implemented with a render prop component (an implementation detail):

import * as React from 'react'
import { mount } from 'enzyme'
import FruitAutocomplete from '../fruit-autocomplete'

// some handy utilities
// learn more about this `sel` function
// from my other blog post: http://kcd.im/sel-util
const sel = (id) => `[data-test="${id}"]`
const hasMenu = (wrapper) => wrapper.find(sel('menu')).length === 1

test('menu is closed by default', () => {
	const wrapper = mount(<FruitAutocomplete />)
	expect(hasMenu(wrapper)).toBe(false)
})

test('lists fruit with a keydown of ArrowDown on the input', () => {
	const wrapper = mount(<FruitAutocomplete />)
	const input = wrapper.find('input')
	input.simulate('keydown', { key: 'ArrowDown' })
	expect(hasMenu(wrapper)).toBe(true)
})

test('can search for and select "banana"', () => {
	const onChange = jest.fn()
	const wrapper = mount(<FruitAutocomplete onChange={onChange} />)
	const input = wrapper.find('input')
	input.simulate('change', { target: { value: 'banana' } })
	input.simulate('keydown', { key: 'ArrowDown' })
	input.simulate('keydown', { key: 'Enter' })
	expect(onChange).toHaveBeenCalledTimes(1)
	const downshift = expect.any(Object)
	expect(onChange).toHaveBeenCalledWith('banana', downshift)
	expect(input.instance().value).toBe('banana')
})

So how do you test a component that uses a render prop component? Whelp, if you're using E2E or Integration tests, you pretty much don't need to do anything different! Just mount your component and interact with it the way you would normally. One thing I should note is that downshift itself is a very well-tested component, so you shouldn't have to test interactions that it provides out of the box. Just focus on what your component is doing. And that's what I'd suggest: test your render prop component really well, then do some high-level tests for the users of the component.

Unit tests

Things get a little tricky with unit tests. If you don't want to include downshift in your tests, then you have to get access to the function you're passing to the render prop. There are a few ways to do this.

The first and most obvious way to do this is to extract the renderprop function and export that:

function FruitAutocomplete({ onChange }) {
	return <Downshift onChange={onChange} render={fruitAutocompleteRender} />
}

// NOTE: this is _not_ technically component, it's _like_ a function component
// but it's not rendered with React.createElement, so it's simply
// a function that returns JSX.
function fruitAutocompleteRender(arg) {
	return <div>{/* what you render */}</div>
}

export { fruitAutocompleteRender }
export default FruitAutocomplete

And now you can import that function directly into your test and use it to render JSX like so:

import * as React from 'react'
import { render } from 'enzyme'

const downshiftStub = {
	isOpen: false,
	getLabelProps: (p) => p,
	getInputProps: (p) => p,
	getItemProps: (p) => p,
}

const sel = (id) => `[data-test="${id}"]`
const hasMenu = (wrapper) => wrapper.find(sel('menu')).length === 1
const hasItem = (wrapper, item) =>
	wrapper.find(sel(`item-${item}`)).length === 1
const renderFruitAutocompleteRenderer = (props) =>
	render(fruitAutocompleteRender({ ...downshiftStub, ...props }))

test('shows no menu when isOpen is false', () => {
	const wrapper = renderFruitAutocompleteRenderer({ isOpen: false })
	expect(hasMenu(wrapper)).toBe(false)
})

test('shows the menu when isOpen is true', () => {
	const wrapper = renderFruitAutocompleteRenderer({ isOpen: true })
	expect(hasMenu(wrapper)).toBe(true)
})

test('when the inputValue is banana, it shows banana', () => {
	const wrapper = renderFruitAutocompleteRenderer({
		isOpen: true,
		inputValue: 'banana',
	})
	expect(hasItem(wrapper, 'banana')).toBe(true)
})

So this works fine. A few things to note:

  • Doing this requires a little less code and is markedly simpler
  • We have to stub out what things downshift passes to us
  • We have to extract the render prop to a separate function and export it

Those second points bother me a fair amount. There's another way to get at the render prop without extracting and exporting it though. Here's that last test implemented as if we didn't export the render prop function:

import * as React from 'react'
import { mount, render } from 'enzyme'
import Downshift from 'downshift'
import FruitAutocomplete from '../fruit-autocomplete'

const downshiftStub = {
	isOpen: false,
	getLabelProps: (p) => p,
	getInputProps: (p) => p,
	getItemProps: (p) => p,
}

test('when the inputValue is banana, it shows banana', () => {
	const fruitAutocompleteRender = mount(<FruitAutocomplete />)
		.find(Downshift)
		.prop('render')
	const wrapper = render(
		fruitAutocompleteRender({
			...downshiftStub,
			isOpen: true,
			inputValue: 'banana',
		}),
	)
	expect(hasItem(wrapper, 'banana')).toBe(true)
})

I also don't really like this because I don't like saying: "Hey, FruitAutocomplete, I know that you use Downshift and that Downshift uses a prop called render." And to me that's diving even further into implementation details.

Also, this still doesn't address my concern of stubbing out downshift. Read more about how I feel about this in this blog post.

There's actually another way we could do this and that would be to use jest.mock to mock the downshift module. But I'm not going to create an example of that because it's no better :)

Conclusion

So I suggest that you just stick with an integration test here and don't bother trying to unit test your render function. I think you'll have more confidence that things wont break if you do.

I should note also that for some components that require a provider to exist (like react-redux or React Router), that you simply render your component within a provider. I have some examples of doing this in my testing workshop for frontend masters.

I hope this is helpful to you! Good luck!

Epic React

Get Really Good at React

Illustration of a Rocket

Testing JavaScript

Ship Apps with Confidence

Illustration of a trophy
Kent C. Dodds
Written by Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. Kent's taught hundreds of thousands of people how to make the world a better place with quality software development tools and practices. He lives with his wife and four kids in Utah.

Learn more about Kent

Want to learn more?

Join Kent in a live workshop

If you found this article helpful.

You will love these ones as well.