Testing React Components

Compressed knowledge from writing thousands of UI tests and a solution for making testing components easy

Photo by Jason Wong

Testing should make us confident. Confident that our software does what we think it does, and that it will continue to do so as we pile up functionality. But testing UI components rarely breeds confidence. Instead, it often makes us feel angry and unproductive.

Why bring this up? Because I’ve had this problem for years, since before the days of React, and I’ve recently put a ridiculous amount of time and thought into solving it. I believe I’ve found a set of tools and practices that make testing React components as easy as writing them.

We’ll start with two core principles and gradually dive into examples.

№1 component=f(props, state) is a lie

It sounds great in theory, but it breaks down when you try to test real life components. As soon as you try to load your components in isolation you realize the following:

component=f(props, state, context)

More concretely,

component=f(props, state, Redux, Router, Theme, Intl, etc)

But this still doesn’t cover the entire spectrum of reality. Namely,

component=f(props, state, context, globals)

Not global variables, only a monster would use those. I mean global APIs.

component=f(props, state, context, fetch, localStorage, window size)
Testing React components is a constant challenge, but very few talk about The Struggle. Official testing examples feature basic components, but taming the Monster Components is mostly taboo. Together we’ll look the beast in the eye and talk about a simple (new) testing API that puts any component to sleep and nicely complements existing tooling like Jest & Enzyme.

I used this pitch to apply to a conference. I didn’t get in (sad trombone ♪), but I meant every word and I still want to talk about it.

So, let’s look the beast in the eye now, shall we?

The following examples use Jest & Enzyme implicitly, but neither is required to use the methods I am about to present.

The usual test example we see is trivial. Perhaps a button with a callback.

Now let’s ensure this component does what we expect it to do.

No wonder testing it feels like a breeze. The component is as dumb as a rock.

Now let’s put a real component to the test. Imagine a basic auth component with a name field and a submit button. You fill in the name and submit, which triggers a fetch request with your data. The response is positive, your name is dispatched to the Redux store and cached in localStorage. Basic stuff.

Try sending this component to a test renderer and this is what you’ll get:

Could not find “store” in either the context or props of Connect…
ReferenceError: fetch is not defined
ReferenceError: localStorage is not defined

To which our usual response is Oh Lawd have mercy!

You could argue that a serious app would have an abstraction for authentication, which should be tested instead. But, as we’ll see in a moment, that has nothing to do with testing components. We should be able to progressively abstract cross-cutting concerns without rewriting component tests.

At this point, based on your experience, you might have two big objections:

  1. Wait, you’re testing “containers”?
  2. Wait, your components interface with global APIs (eg. fetch)

Yes and why not.

And I also don’t use shallow rendering. Which leads us to the next principle.

№2 Beware of the “glue” between your units

Even with the simple component model of React and libraries alike, components are complex entities, far more so than plain functions. So we’re tempted to extract units as pure as possible. But the more granular we design our units, the more space for integration error we expose.

Eg. We commonly export components without their wrappers to test them alone. Which is fine, we say, because the HoCs are already tested and we can test the connecting parts. We test reducers, actions and selectors. Hell, we should even test mapStateToProps and mapDispatchToProps for complete coverage. I used to follow this school of thought, but in time realized its major flaws. So many that I dedicated a paragraph to each.

  • Effort to test is greater than effort to implement. Good luck onboarding developers: Write your first component in Day 1. Struggle to write tests for it three days later.
  • It discourages refactoring. This might be the biggest issue for me. It’s not uncommon to dig our own grave when developing software, and there’s no better way to cement an average codebase than by meticulously assigning meaning to and testing every single function.
  • More room for error. It’s enough to forget testing any small part (eg. some selector) and the entire component can malfunction even when all tests are passing.
  • Out of sync inputs and outputs. The component can react properly to some props, and mapStateToProps can return correctly based on some state, but the exported component will still fail if the props from the former test get out of sync with the return value from the latter.

It’s tempting to seek comfort in our neat little units. But if we only test the basic parts we end up outsourcing our app’s complexity — the glue that keeps everything together — to chance. It’s also tempting to let QA worry about integration, but E2E tests are a safety net, operating on a much higher level.

The less glue I leave out of my component tests the more confident I am about their output, but also in the ability to reuse them. The real unit is what we plan to share and reuse. The real unit bears meaning for the end user. That’s what we should test, not what is more convenient.

OK, but the test setup is overwhelming…

Setting up all the providers and mocks for “smart” components requires most of the effort and LOC in a test file. Asserting is piece of cake once the component mounts successfully. So my question became: How to simplify the test setup and let developers focus on asserting behavior?

If only there was an easy way to simulate component states by mocking inputs…
Cosmos Playground

Actually, there is!

Cosmos fixtures were designed to mock every input and render components under any combination of state. And while for a long time they were only used inside the Playground UI, it became evident that they can also replace elaborate test setup—a two-in-one offering!

What’s a fixture?

First, let’s see what a JSX tag is (or React.createElement under the hood).

<Button disabled={true}>Click me maybe</Button>

It’s a declaration. In this case, it says: I want a the Button component with the { disabled: true } props and the Click me maybe children.

Now, imagine a fixture as a React element on steroids…

Besides Component, props and children, it can receive local state, Redux state or Router URL. Fixtures can also mock fetch, XHR or localStorage.

All these features are driven by plugins, which make the fixture a platform for mocking component states.

Fixtures are regular JS objects, like this one.

{
props: {}
  url: '/dashboard',

localStorage: {
name: 'Dan'
},

reduxState: {},

fetch: [
{
matcher: '/api/login',
response: {
name: 'Dan'
}
}
]
}

Once you get used to writing these, not only will you get a component-centric dev tool for free, but writing component tests will become easy and fun. Which leads us to the newly released Cosmos Test API.

Fun fact: The Cosmos UI is tested using the Cosmos Test API

Here’s a sample of what the API looks like, but please read the docs and try it out to get a taste of its power.

The goals for this abstraction were to be as thin as possible and work well with standard testing practices. I’m not completely content with the “context” naming, but I’m happy to report that the API has been peer reviewed by a number of people and battle tested at ScribbleLive, the company I’m currently consulting for.

I’m grateful for the opportunity to design this against a real codebase and I especially thank Thomas Mattacchione for playing a pivotal role in this development. Credit for the refined surface level API goes to Xavier Cazalot.

And thank you for making it so far!

Before I end, I’ll share two anecdotes from my experience writing tests using the aforementioned principles and API.

  1. Even in mature codebases, I usually write ad-hoc prototypes for new features. Instead of plugging into the Redux store from the start, I might start with local state until it gets serious. More than once, I completed a fully tested prototype and then ported the local state to Redux without rewriting any test.
  2. A stateless Form component was used on many screens. A nice abstraction, but every instance required data mapping boilerplate and lifecycle ceremony. So I wrote FormConnect to abstract the repetitive parts. Again, refactored a dozen app screens without rewriting any test. Why? Because those screens hadn’t changed!

For convenience, here’s another link to the Cosmos Test API.

I hope you get to experience the joy and confidence brought by testing React components with ease!

PS. You might also like When can I assert?, a complementary post on how to test async code, especially relevant in UI component testing.