Level Up Your First React App: Pt 2

Yep. You guessed it: Tests.

For some background on this series, check out the beginning (or all) of part one.

When I was first exposed to my first exposed to testing, specifically TDD, it was described as similar to exercise: we all know it’s good, but that doesn’t mean we always do it.

As I built my first React app, I let myself off the TDD hook with the mindset of:

THIS IS ALL NEW STUFF SO I RESERVE THE RIGHT TO JUST HACK THIS TOGETHER AND NOT TEST!

While I still defend that perspective, I do believe testing in React is not as difficult as I made it out to be at the time. My goal here is to guide you to the same conclusion. In what follows, I cover:

  1. Dependencies
  2. File setup
  3. Truth-y render tests
  4. Snapshot tests
  5. Testing with mocks
  6. Unit tests

Let’s get to it.


Dependency context ✍️

In terms of managing my dependencies via Webpack or other build tools, I’ve never built a React project from scratch. Coming from the Rails world of “convention over configuration”, the allure of Facebook’s Create React App (CRA) tool was too strong to resist.

A CRA base comes with Jest baked in to react-scripts , which serves as the test runner and assertion library. You have Jest’s expect() function available by default. To test components, Enzyme is basically a must-have for your development. It provides three functions to simulate component rendering: shallow , mount , and render . Read the docs for a more nuanced breakdown, but you typically should always start with shallow and only change to mount or render if you run into problems.

So we have our testing base: Jest runs our tests and makes assertions, and Enzyme tests our components and their manipulation on a test DOM. But what should you be testing, and how should you go about doing so?

I 100% avoid any and all claims to what the best or proper ways to test in React are, the following just comprise what I’ve found to be a helpful process for having peace of mind about the functionality of my application.

Here are the relevant dependencies in my current package.json for the source of all my examples:

{
"private": true,
"devDependencies": {
"enzyme": "^2.7.1",
"jest-enzyme": "^2.1.2",
"react-dom": "^15.6.1",
"react-test-renderer": "^15.6.1",
},
"dependencies": {
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-scripts": "1.0.10",
},
}

If patterns, methods, setups, &c I use don’t work for you, this is probably your best place to start troubleshooting.

Where to put your test files 🔎

I’ve seen many suggested ways for organizing React directories in terms of things like components, stylesheets, tests, &c.

Mostly to switch things up and do something not Rails-y, I went with the self-contained component approach, where the component, test, and stylesheet all live together in a folder of the component’s same name.

E.g.,

src/
| App/
| | Contact/
| | | Contact.jsx
| | | Contact.spec.js
| | | _contact.scss
| | | // child components of Contact and other dependencies

If this doesn’t jive with you, Jest can handle other organizational patterns. From the CRA docs:

Jest will look for test files with any of the following popular naming conventions:
Files with .js suffix in __tests__ folders.
Files with .test.js suffix.
Files with .spec.js suffix.
The .test.js.spec.js files (or the __tests__ folders) can be located at any depth under the src top level folder.

Required imports 📦

Most of your test dependency setup will look something like this:

// src/App/Contact/Contact.spec.jsx
import React from "react"
import { shallow } from "enzyme"
import Contact from "./Contact"
  1. React , the top-level API import for things like JSX markup.
  2. shallow for simulating your component rendered on the DOM. If you’re wondering what’s going on with the { shallow } syntax, check out JavaScript’s destructuring assignment.
  3. Contact is the component being tested.

Expect a truth-y rendering 🖥

At a minimum, you can get some momentum with the Enzyme docs classic:

// src/App/Contact/Contact.spec.js
// imports from snippet above ^^^
...
describe("<Contact />", () => {
const contact = shallow(<Contact />)
  it("renders a contact page", () => {
expect(contact).toBeTruthy()
})
  ...
})

If the component is free of syntax and dependency errors, and is exported correctly, this test will pass.

I started with these types of tests because (a) they were in the documentation, and (b) it was a confidence boost to have a passing test! In the back of my mind though, I questioned their utility.

However, as my application grew and natural changes occurred like names or locations of files, these tests would fail and let me know that I didn’t have my “wires” connected as expected/needed.

Because they are so simple, their failures are very significant and worth monitoring.

Snapshot icing on the JSX cake 🍰

While working on this post, and going through this project and its commit history for examples, I unearthed some of my original markup-oriented tests. For components with static data, it’s pretty easy to basically test-drive your JSX markup with something like this:

// src/App/Landing/Landing.spec.js
...
const landing = shallow(<Landing />)
it("renders the correct content", () => {
expect(landing.equals(
<section className="landing">
<article className="logos">
<Link to="map"><img src={map} alt="map"/></Link>
        ...
      </article>
        ...
    </section>
)).toBeTruthy()
})

(before the component’s markup is written).

At first, I thought this was cool. I could write my markup in the test, and just copy it over to the component once I was ready. As requirements changed however, this got really tedious and annoying. Every change to the markup required a change in the test.

I realized that markup isn’t that great of a domain for TDD and/or test-as-documentation like functions and object behavior are. I don’t need my test to document how the component is structured; the component documents that.

Encouraged by this great post, I leaned into component first, “snapshot” second testing.

Here’s the setup for a Jest snapshot test:

// src/App/Landing/Landing.spec.js
describe("snapshot", () => {
it("is valid", () => {
expect(landing.getNodes()).toMatchSnapshot()
})
})

The first time toMatchSnapshot()is called in a test, it creates a __snapshots__ directory in the same directory as the test file, and adds a .snap file to that directory. The .snap file is basically a blueprint for the component’s markup shape/structure. All subsequent test runs compare the mounted component to the snapshot.

Only once you are satisfied with your component’s markup do you write the snapshot test (otherwise you’ll just be updating the snapshot all the time). If snapshot testing and the point of it are still fuzzy, let the name guide you: when you’ve built something cool, you take a snapshot to, I don’t know, show your grandparents?

You may be thinking:

Asserting that the component looks that the way that it looks? This seems like a silly song-and-dance of testing…

I had similar thoughts at first, but, again, this is not TDD. I really like snapshots now. Similar to the expect(someComponent).toBeTruthy() test, the snapshot is about monitoring the component as it changes over time.

Whenever a component’s markup is changed, the snapshot test will fail. That’s okay. If you see that failure and think “…yeah…I know. I just changed that component…”, fear not: there’s a simple command-line command that lets you just rewrite the snapshot.

If you didn’t intend for the component to change however, the snapshot test serves dutifully to notify you that something has gone awry (I totally guessed on that spelling and got it on the first try!).

While snapshots don’t drive development, they still define expectations for your app and hold you accountable to them over time.

Mocks. Was the function called or not? 📞

For functions that call other functions, I just want to make sure those calls are happening as I expect them to. I don’t need to see if they produce all the side effects they are supposed to.

As long as I (1) test that all the smaller functions within a more complex function are called and (2) unit test each of those smaller functions, I am confident that the two levels of tests combined prove the app’s functionality.

Given a component for rendering an Instagram feed, here is an example of how I approach mocking functions using Jest’s jest.fn() :

// src/App/Instagram/Instagram.jsx
import React, { Component } from "react"
import APIService from "../APIService/APIService"
const apiService = new APIService("https://spoken-api.herokuapp.com")
export default class Instagram extends Component {
componentDidMount() {
this.props.actions.fetchPhotos(apiService)
}
render() {
const photos = this.props.photos.map((photo, i) => {
return (
<div key={i} >
          ...
        </div>
)
})
return (
<div className="instagram">
<article className="photos" >
          ...
          { photos }
</article>
</div>
)
}
}

I would test componentDidMount like this:

// src/App/Instagram/Instagram.spec.js
import React from 'react'
import { shallow } from 'enzyme'
import Instagram from './Instagram'
const fakeActions = {
fetchPhotos() {}
}
const fakePhotos = ["AHH!", "YEAHH!"]
describe('<Instagram />', () => {
const wrapper = shallow(
<Instagram actions={fakeActions} photos={fakePhotos} />
)
  ...
  describe("componentDidMount", () => {
it("calls the fetchPhotos action", () => {
// 1
const instagram = wrapper.instance()
const restore = instagram.props.actions.fetchPhotos
const mock = instagram.props.actions.fetchPhotos = jest.fn()
      // 2      
instagram.componentDidMount()
expect(mock).toHaveBeenCalled()
      // 3
instagram.props.actions.fetchPhotos = restore
})
})
})
  1. Assign variables for the component instance, the original function to be mocked, and the mocked version of the function.
  2. Execute the function described in the test block and assert that the mock was indeed called.
  3. Restore the mocked function back to its original definition.

When using jest.fn() over alternatives like spyOn , mocks have to be set and reset manually. It might be a bit more typing, but I actually kind of like the symmetry of the setup and teardown.

Notice too that I didn’t need to use Enzyme’s mount function. I trust that React components invoke lifecycle functions like componentDidMount when necessary, so I can simply test the effects of those functions by calling them directly.

Elsewhere, I test the Redux action and corresponding reducer to ensure they do what they’re supposed to do (these tests will be covered in a future installment of the series, where I’ll cover how to add Redux to an existing React app). I don’t need to see if componentDidMount adds photos to the Redux store or to the DOM because look at the body of the function: it doesn’t do those things. It just calls an action passed down in the props object.

If we’re talking data or state, let’s talk unit tests ⚙️

In my experience, the other main types of functions in React are ones that take data and return a processed form of it, or set a component’s state. For functions like these, it’s time to get down to unit testing. For input/output (i.e., purely functional) functions, the unit tests will look similar to other unit tests you’ve (hopefully) written in other contexts.

Here is a straightforward example of a function that takes a number, and returns a that number with 11 decimal places (useful for comparing latitude and longitude points):

// src/App/Map/Map.jsx
import React, { Component } from "react"
...
export default class Map extends Component {

...
  elevenDecimalPlaces = number => (
parseFloat(parseFloat(number).toFixed(11))
)
  ...
}

And here are its unit tests:

// src/App/Map/Map.spec.js
import React from 'react'
import { shallow } from 'enzyme'
import Map from './Map'
...
const suggestions = [
{
location: { lat: 10.0001, lng: 10.0001 },
description: "sweet", label: "Place to stay", category: "stay"
}
]
const props = {
actions: {
fetchSuggestions () {},
addCurrentSuggestion () {},
toggleSuggestionInfo () {},
fetchCurrentLocation () {},
fetchRoutePoints () {},
fetchActualPath () {}
},
suggestions: suggestions,
pinFilters: [],
}
describe('<Map />', () => {
  ...
  const map = shallow(<Map {...props} />).instance()
  ...
  describe("#elevenDecimalPlaces", () => {
const { elevenDecimalPlaces } = map
    it("rounds a number with 4 decimal places to 11 decimal places",
() => {
expect(
elevenDecimalPlaces(10.0234)
).toEqual(10.02340000000)
})
    it("rounds a number with 15 decimal places to 11 decimal places",
() => {
expect(
elevenDecimalPlaces(10.023491829833019)
).toEqual(10.02349182983)
})
    it("brings a whole number to 11 decimal places", () => {
expect(elevenDecimalPlaces(10)).toEqual(10.00000000000)
})
})
  ...
})
...

Enzyme’s shallow is still used to render an instance of the component, but the rest of the test is straightforward function invocation and Jest assertions.

Testing functions that manipulate state are similar, just a little more involved. Here is a stateful form component:

// src/App/Map/SideWrapper/SuggestionForm/SuggestionForm.jsx
import React, { Component } from "react"
...
export default class SuggestionForm extends Component {
constructor(props) {
super(props)
this.state = {
pin: {
name: "",
email: "",
label: "",
description: "",
category: "",
message: "",
},
      ...
    }
this.clearedForm = this.state.pin
}
  handleChange = event => {
this.setState({
pin: {
...this.state.pin,
[event.target.name]: event.target.value
}
})
}
  ...
}
...

handleChange is the callback function used anytime a change event is fired on a form input. The CSS name property values on the inputs line up with the this.state.pin property names.

I want to test to make sure that when given an event object, handleChange updates state for a the component.

// src/App/Map/SideWrapper/SuggestionForm/SuggestionForm.spec.js
import React from "react"
import { shallow } from "enzyme"
import SuggestionForm from "./SuggestionForm"
const props = {
suggestionPin: {},
actions: {
addSuggestionPin: function () {},
fetchSuggestions: function () {}
}
}
describe("<SuggestionForm />", () => {
const wrapper = shallow(<SuggestionForm {...props} />)
const suggestionForm = wrapper.instance()
  ...
  describe("handleChange", () => {
it("sets state for pin information", () => {
expect(suggestionForm.state.pin.email).toEqual("")
suggestionForm.handleChange(fakeEvent)
expect(suggestionForm.state.pin.email)
.toEqual("tommy@crosby.com")
})
})
  ...
})
const fakeEvent = {
preventDefault () {},
target: {
name: "email",
value: "tommy@crosby.com"
}
}
...

For state changes, I like to expect values before and after the described function. Here, I am 100% confident that handleChange updates state, given a valid event.


I hope these examples help shed some light on how you might approach testing in React. If you’re already far along in a project, I recommend adding test coverage for your components in this order:

  1. The it("renders without crashing", () => {}) pattern
  2. Snapshot tests (if the markup is ready)
  3. Unit tests for functions that process data or manipulate state
  4. The expectMockToHaveBeenCalled pattern for complex functions that invoke smaller functions
  5. Snapshot tests (if the markup is not ready)

On future functionality and future projects, I recommend:

  1. Step 1 from above
  2. Test-drive functions that process data, manipulate state, or call other functions
  3. Write a snapshot test once the markup is complete
  4. (Iterate on steps 2 and 3 as needed)

Taking the time to do so opens you up to a range of benefits: employing CI/CD tools like Travis, better understanding of your application, and overall better sleep at night (that’s a scientific fact).

Thanks for tuning in. I’d love to hear any feedback you might have. Until next time…

#learnAndGrow