This site runs best with JavaScript enabled.

Testing ⚛️ components using render props

January 08, 2018

Video Blogger

Photo by Scott Walsh on Unsplash


Let's look at how we can write tests for React components that use render props!

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:

1import React from 'react'
2import {render} from 'react-dom'
3import Downshift from 'downshift'
4
5const items = ['apple', 'pear', 'orange', 'grape', 'banana']
6
7function FruitAutocomplete({onChange}) {
8 return (
9 <Downshift
10 onChange={onChange}
11 render={({
12 getInputProps,
13 getItemProps,
14 getLabelProps,
15 isOpen,
16 inputValue,
17 highlightedIndex,
18 selectedItem,
19 }) => (
20 <div>
21 <label {...getLabelProps()}>Enter a fruit</label>
22 <input {...getInputProps()} />
23 {isOpen ? (
24 <div data-test="menu">
25 {items
26 .filter(i => !inputValue || i.includes(inputValue))
27 .map((item, index) => (
28 <div
29 {...getItemProps({
30 key: item,
31 'data-test': `item-${item}`,
32 index,
33 item,
34 style: {
35 backgroundColor:
36 highlightedIndex === index ? 'lightgray' : 'white',
37 fontWeight: selectedItem === item ? 'bold' : 'normal',
38 },
39 })}
40 >
41 {item}
42 </div>
43 ))}
44 </div>
45 ) : null}
46 </div>
47 )}
48 />
49 )
50}
51
52export 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

Sorry, no example of E2E tests today. Maybe when I publish this to > my blog I'll have some...

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):

1import React from 'react'
2import {mount} from 'enzyme'
3import FruitAutocomplete from '../fruit-autocomplete'
4
5// some handy utilities
6// learn more about this `sel` function
7// from my other blog post: http://kcd.im/sel-util
8const sel = id => `[data-test="${id}"]`
9const hasMenu = wrapper => wrapper.find(sel('menu')).length === 1
10
11test('menu is closed by default', () => {
12 const wrapper = mount(<FruitAutocomplete />)
13 expect(hasMenu(wrapper)).toBe(false)
14})
15
16test('lists fruit with a keydown of ArrowDown on the input', () => {
17 const wrapper = mount(<FruitAutocomplete />)
18 const input = wrapper.find('input')
19 input.simulate('keydown', {key: 'ArrowDown'})
20 expect(hasMenu(wrapper)).toBe(true)
21})
22
23test('can search for and select "banana"', () => {
24 const onChange = jest.fn()
25 const wrapper = mount(<FruitAutocomplete onChange={onChange} />)
26 const input = wrapper.find('input')
27 input.simulate('change', {target: {value: 'banana'}})
28 input.simulate('keydown', {key: 'ArrowDown'})
29 input.simulate('keydown', {key: 'Enter'})
30 expect(onChange).toHaveBeenCalledTimes(1)
31 const downshift = expect.any(Object)
32 expect(onChange).toHaveBeenCalledWith('banana', downshift)
33 expect(input.instance().value).toBe('banana')
34})

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:

1function FruitAutocomplete({onChange}) {
2 return <Downshift onChange={onChange} render={fruitAutocompleteRender} />
3}
4
5// NOTE: this is _not_ technically component, it's _like_ a function component
6// but it's not rendered with React.createElement, so it's simply
7// a function that returns JSX.
8function fruitAutocompleteRender(arg) {
9 return <div>{/* what you render */}</div>
10}
11
12export {fruitAutocompleteRender}
13export default FruitAutocomplete

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

1import React from 'react'
2import {render} from 'enzyme'
3
4const downshiftStub = {
5 isOpen: false,
6 getLabelProps: p => p,
7 getInputProps: p => p,
8 getItemProps: p => p,
9}
10
11const sel = id => `[data-test="${id}"]`
12const hasMenu = wrapper => wrapper.find(sel('menu')).length === 1
13const hasItem = (wrapper, item) =>
14 wrapper.find(sel(`item-${item}`)).length === 1
15const renderFruitAutocompleteRenderer = props =>
16 render(fruitAutocompleteRender({...downshiftStub, ...props}))
17
18test('shows no menu when isOpen is false', () => {
19 const wrapper = renderFruitAutocompleteRenderer({isOpen: false})
20 expect(hasMenu(wrapper)).toBe(false)
21})
22
23test('shows the menu when isOpen is true', () => {
24 const wrapper = renderFruitAutocompleteRenderer({isOpen: true})
25 expect(hasMenu(wrapper)).toBe(true)
26})
27
28test('when the inputValue is banana, it shows banana', () => {
29 const wrapper = renderFruitAutocompleteRenderer({
30 isOpen: true,
31 inputValue: 'banana',
32 })
33 expect(hasItem(wrapper, 'banana')).toBe(true)
34})

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:

1import React from 'react'
2import {mount, render} from 'enzyme'
3import Downshift from 'downshift'
4import FruitAutocomplete from '../fruit-autocomplete'
5
6const downshiftStub = {
7 isOpen: false,
8 getLabelProps: p => p,
9 getInputProps: p => p,
10 getItemProps: p => p,
11}
12
13test('when the inputValue is banana, it shows banana', () => {
14 const fruitAutocompleteRender = mount(<FruitAutocomplete />)
15 .find(Downshift)
16 .prop('render')
17 const wrapper = render(
18 fruitAutocompleteRender({
19 ...downshiftStub,
20 isOpen: true,
21 inputValue: 'banana',
22 }),
23 )
24 expect(hasItem(wrapper, 'banana')).toBe(true)
25})

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!

Things to not miss:

Discuss on TwitterEdit post on GitHub

Share article
loading relevant upcoming workshops...

TestingJavaScript.com

Your essential guide to flawless testing.

Jump on this self-paced workshop and learn the smart, efficient way to test any JavaScript application. 🏆

Start Now
Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. He'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.