Skip to content

Instantly share code, notes, and snippets.

@jdiamond
Created August 16, 2017 21:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jdiamond/5bfb28bad70db4e70d9727b630502b13 to your computer and use it in GitHub Desktop.
Save jdiamond/5bfb28bad70db4e70d9727b630502b13 to your computer and use it in GitHub Desktop.
Testing React apps and components

"Injecting" configuration into a create-react-app is done with environmental variables. I always have at least one for the root of my REST API or GraphQL endpoint.

When running in development, I set that variable to point at a local version of the backend server I have running on my laptop. That's some other project that I do whatever I need to in order to get it running and available to accept requests from my React app.

If I wanted to start simple with static JSON files, maybe I would use something like json-server, but I normally have an existing backend already available for my apps.

There's also nothing stopping you from just putting static JSON files in the public directory and setting your variable to those filenames. They will be fetched over the local "network" but big deal.

The kind of testing I'm doing here is ad-hoc in nature and I won't even notice the cost of hitting the network because it's all on localhost.

To do this, stick your variables in a .env file (they have to start with REACT_APP_):

REACT_APP_DATA_URL=data.json

Then use them in your code like this:

export default class SomeComponentThatFetchesData extends Component {
  state = { data: [] };

  async componentDidMount() {
    const response = await fetch(process.env.REACT_APP_DATA_URL);

    if (response.ok) {
      this.setState({ data: await response.json() });
    }
  }

  render() {
    return <SomeComponentThatPresentsData data={this.state.data} />;
  }
}

Unfortunately, it's now impossible to test that component without accessing the network. That's fine for exploratory testing, but not unit testing.

First of all, I don't normally write unit tests for React components. When I do, I understand I have to write them in a style that makes them as easy-to-test as possible or I'll start making excuses and stop writing tests.

Standard "best practices" apply in JavaScript just like in every other programming language. Complicated business logic shouldn't be embedded in user interface code. Low-level dependencies (like networking and data access) should be encapsulated in some sort of abstractions.

To me, that means complicated "business logic" gets written in (or extracted to) their own modules so that they can be easily tested. Networking/data access logic likewise goes in their own modules. The actual data is usually passed in to the business logic modules via normal function or constructor arguments making them very easy to test.

OK, so let's say I actually want to test a React component. That component needs to be written in a way that's testable.

Components that use the network like I showed above can't be easily tested. But did you notice how that component didn't actually render anything? All it does is fetch data and leaves the rendering to some other component.

Being able to nest (compose) components like this is what makes React so awesome. It's easy to think of this as a purely a visual thing, but I think it's also a way to structure my code.

Abstractly, I think of my application as a tree of components with the root component as the "core" of my application. It's the most application-specific component in my entire app and can never be reused because it depends on every single other component it indirectly contains (is composed of).

If I think of the component tree as a dependency tree, it's even easier for me to visualize nodes closer to the root as having more dependencies than leaf nodes and are, therefore, harder to test.

For unit testing (and many other) purposes, leaf nodes are the best kinds of components. Those components have no dependencies except for the props passed in to them by their parent components.

These components are like functions that don't use any global variables and only operate on the arguments passed in to them. Testing functions like that is so easy.

In React, there's this concept of presentation components and container components. You can tell by the name that container components contain things so they can't be leaf nodes. Presentation components are the opposite. They're meant to be contained. Their job is to present data passed in to them via props.

To make things very confusing, presentation components can contain container components and that's totally fine. It will eventually click.

The component I showed above is a container component. This is what the presentation component it uses might look like:

export default class SomeComponentThatPresentsData extends Component {
  render() {
    return (
      <ul>
        {this.props.data
          .filter(item => item.visible)
          .map(item => <li key={item.id}>{item.value}</li>)}
      </ul>
    );
  }
}

See how this component never fetches data? The data is basically "injected" via props. That makes this component trivial to test:

import React from 'react';
import { shallow } from 'enzyme';

import SomeComponentThatPresentsData from './SomeComponentThatPresentsData';

describe('<SomeComponentThatPresentsData />', () => {
  it('only renders visible items', () => {
    const data = [
      { id: 1, visible: true, value: 'foo' },
      { id: 2, visible: false, value: 'bar' },
      { id: 3, visible: true, value: 'baz' },
    ];

    const wrapper = shallow(<SomeComponentThatPresentsData data={data} />);

    expect(wrapper.find('li').length).toBe(2);
  });
});

All network access (even to localhost) is completely avoided and this test runs as fast as it can. I can even see everything I need to in order to reason about what this test is doing because every piece of data the component uses is right here in the test itself.

I made the decision to make my presentation component testable, but not my container component. I'm not saying I never test container components. create-react-app generates a test to ensure <App /> doesn't crash when rendering. I think that's cool and try to keep that test passing, but if that ends up being difficult, it gets deleted. My <App /> component usually ends up being where all the routes are set up so not usually something that needs to be tested, IMO.

There is nothing in JavaScript or React that enforces any of this, though. It’s just a style that works for me that I learned from the community.

Final note: I really like Redux. When I use Redux with React, my code looks a little different than what I wrote above.

The react-redux package has a connect() function that "injects" props into components. It takes a small amount of "boilerplate" to wire everything together, but I think it's reasonable and like how explicit it requires me to be. I kind of think of connect() as generating my container components for me.

I do not recommend learning React and Redux at the same time.

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