If you want to write tests for React components, you currently have about 3 choices:
- Wing it
- Render to the DOM (hopefully emulating the DOM with something like jsdom or domino), and test the HTML
- Use the shallow renderer
Let’s assume you actually want to sleep at night, so winging it is not an option. Rendering to the DOM and then testing the HTML allows you to use the React.addons.TestUtils.Simulate to simulate events, and then check the DOM for the expected result.
If your component is large, you are free to refactor it into sub-components, and the tests will still pass. Being able to refactor code when the tests are green, and staying green whilst you’re doing it, is a very nice feature.
However, testing that DOM elements exist with the correct attributes — or don’t, depending on your tests — can be taxing. Libraries such as unexpected-dom can really help here to assist checking elements in the DOM, and reporting a nice diff if things aren’t working.
To demonstrate, the following test simulates a click on a rendered component, and then asserts the rendered HTML using unexpected-dom. The formatted failure message is shown below.
Using the shallow renderer makes testing more straightforward, as you can explicitly test a single component and what the render method gives back, ignoring the dependent components. Firing events is harder, as you have to reach in and find the relevant prop where the event is attached and call it with your own simulated event.
I wrote unexpected-react-shallow, a plugin for the excellent unexpected assertion library (really, if you’ve not tried it yet, you should) to enable writing assertions directly with JSX syntax. Any differences would be highlighted in JSX, not as the huge component object.
Other libraries have also come out, such as expect-jsx, which convert the component objects to a JSX string, which allows for string diffing. This isn’t quite as powerful as unexpected-react-shallow, which does the diffing at the JSX level, but allows you to stick with expect.js if that’s your thing.
And here’s the error output from unexpected-react-shallow
But, with the shallow renderer, you lose the ability to refactor to sub-components without changing your tests at the same time. Maybe that’s ok, maybe it isn’t.
We need an option 4
What would be really nice, is if we could have the best of both worlds.
- Use the React.addons.TestUtils.Simulate to simulate real events
- Render the component in its entirety, and test just the bits of the output that we’re interested in
- Optionally ignore children of sub-components (as in the shallow renderer), so we can test the component in isolation
To do this, we need to test the virtual DOM. With access to the full virtual DOM, we could simulate events in the real (or emulated) DOM, and test the changes in the custom component props, optionally checking what the custom components themselves render.
But how to get at it, it’s not exposed, there’s no API. We could obviously dig into the internals of React (side note: I tried this, it’s not pretty, and the possibility might be going away), but that’s going to be very brittle, and depend on things we really don’t want to depend on.
But there is a virtual DOM display, and anyone that develops on React has almost certainly used it.
The React devtools. The new version, as everyone agrees, is brilliant, responds to updates, has multi-browser support, allows updating state directly, gives access to the component in the console and and and…
It’s also got a nice design, with a backend that hooks into React and monitors the changes, and critically, the virtual DOM. Utilising this, means there’s only one thing to maintain.
Skip forward 2 months, lots of hacking, lots of assistance, and we have unexpected-react. A plugin for unexpected that can test against the virtual DOM. In order to use the React.render() method, we still need the DOM available, so we still need to use jsdom or domino.
The hooks into React are extracted to the package react-render-hook, for anyone who wants to build a similar package for another assertion library. The diffing has also been extracted to unexpected-htmllike, so that other HTML/XML style plugins for unexpected can benefit from the heuristics based diffing.
So, what can we do with it?
Let’s say we’ve got a simple combobox component, called Select. It renders options when clicked in an <li> element. The options themselves are based on a sub-component, SelectOption.
The first two tests are pretty trivial, and we could have done them with the shallow renderer. Notice we’re using the normal TestUtils.Simulate.click() method to simulate the click to open the combobox.
As we’re only testing that the SelectOption components are rendered, we could still have done that with the shallow renderer. It’s worth noting that all these assertions work equally well with the shallow renderer, so unexpected-react-shallow is no longer needed. The output from unexpected-react also has several improvements over unexpected-react-shallow.
However, if we want to test that the actual <li> elements are rendered, we’d need to test that the SelectOption renders correctly. This would be (IMHO) the right way to do it, by testing SelectOption as an independent component, but, if you start off rendering <li> elements directly from your component, you can’t then refactor to use a sub-component, without changing your tests.
When you write tests against the virtual DOM, you can do both, like the following test shows, purely testing the HTML.
(If your eye-based HTML linter is screaming “there’s no UL”, hold that thought.)
Let’s say we’ve got a silly off-by-1 error, and only the first option is rendered, here’s the error message:
You can see how it has highlighted the missing <li> element, and also shown the <SelectOption> and <ul> (it is there!) in grey. The SelectOption and ul elements are greyed out because it’s noticed that they are just wrapper elements, and what was referred to in the test, is the <li> element that is there and correct.
It works this out using a series of weights for various differences, and attempting to find the best match. If you want to specifically ensure that there are no wrappers, you can use the assertion ‘to have exactly rendered’, which also checks that there are no extra props, or ‘to have rendered with all wrappers’, which doesn’t allow any extra wrappers to be added.
Actually, testing the entire tree is rendered correctly is a fairly brittle test, and not something we’d want to do very often. What is more often required, is to check that some element or component appears in the rendered tree somewhere. That’s where the assertion ‘to contain’ comes in:
Since we internally have weightings, we can actually show the best match if this fails. Here we’re checking that the <li> is rendered with the data-value attribute (this is purely for example, obviously data-in-the-dom is a Really Bad Idea™)
Hopefully you’ve noticed that our <li> elements are rendered with a random unique id attribute. We’d like to test that they’re also rendered. Here’s where the power of unexpected really comes in
By inlining the expect.it(…) assertion, we can check specific characteristics of an attribute or content string, and if there’s a mismatch, we see all the issues together.
Plugins in unexpected can be easily combined, so in a QR code generator component, it would be possible to test that the image produced resembles a reference image, using unexpected-resemble, and an inline check of the data-url in the <img> tag.
I’m ending with getting started. Here’s the requires you need, in order to use unexpected-react. The order that the DOM emulation library, unexpected-react and react are required in is important.
React checks if a global object is defined, in order to install the hook for the devtools — normally devtools is hooked into the page before the page code starts, so this isn’t an issue, but as we’re outside the browser, we need to ensure the object is there before React starts up. As react-render-hook needs the DOM, we need to ensure the global window object is defined before the hook is defined, and before React decides if it has a DOM, and can therefore enable the React.render(…) method.
The ‘emulateDom.js’ file just creates the window and document globals. I’m using domino here, the jsdom equivalent is shown below, whichever you prefer.
npm install --save-dev unexpected-react
If you’d like to checkout a sample project, clone https://github.com/bruderstein/unexpected-react-example
Really looking forward to hearing your feedback — if you have any issues, just raise an issue on github.
Many thanks to all the people from the unexpected project, especially Sune Simonsen and Andreas Lind, who helped out, answered questions, gave suggestions and feedback, and read drafts of this post. Also, a huge thanks to Peter Müller for introducing me to unexpected, and writing unexpected-dom, where much of the output and diffing code originates, and together with Domenico Matteo from Podio, for handing over the unexpected-react name in npm, and also giving feedback on drafts of this post.