Sitemap

Good Practices for Testing React Apps

8 min readAug 17, 2016

--

Alright, hot off the heels from reading GOOS, I have some insights on creating testable React apps.

Start with an end-to-end test

First and foremost, start every feature with an end-to-end test. An end-to-end test is basically a test from the user’s perspective (click here, wait, confirm this text is displayed, etc.) The go-to library for end-to-end tests is Selenium.

NOW, setting up Selenium is a special kind of hell. There are like, 8 libraries for it, and they all kind of suck. I tried the vanilla node library (selenium-webdriver on npm), and thought it was a little too verbose and the docs a little too murky, so I switched to webdriverio, which was like trading a rotting apple for a fermenting orange. It’s only marginally better.

Here’s an (slightly-modified) actual webdriverio test from one of my projects:

Okay yeah, there’s a lot of stuff going on here.

FIRST, from a high level, this test opens the app, opens the login page, enters a phone number, enters a 6 digit code, and then confirms that the intro is shown.

It’s important to note how idiomatically this reads. That’s because I’m using a pattern called PageObjects here, which Fowler talks about over here. Essentially you create a PageObject that represents a page in your app, that encapsulates all the nasty Selenium stuff.

Here’s what the App PageObject looks like:

Now, look at `get self()`. I’m using an attribute selector on data-testid. data-testid is a convention taken from React Native, which uses the testID prop to identify components in e2e tests. react-native-web took up that convention, and uses, yep, data-testid to identify components. I like it 🙂

SECOND, unabashedly using async/await everywhere. This is a super good use-case for it. End-to-end tests are the most async of async tests, and async/await makes it very clear what’s going on. Selenium libraries (webdriverio included) generally maintain their own call stack, so you don’t need to use promises and async/await. But relying on this has caused wayyy too many headaches for me and I’ve found using promises everywhere is much more reliable in already-flaky tests.

Finally, do you notice anything interesting about the phone number it’s using to log in with?

It’s labelled PHONE_BYPASS. This is a special phone number I’ve set up that lets me bypass the login screen. This lets me basically completely bypass login.

This is super important: creating a fast way of logging in, whether that’s special login credentials or a pre-authed session cookie. This brings me to the next point in e2e tests:

Decouple your end-to-end tests, so they can run in parallel.

End-to-end tests are really slow, and really flaky, so it’s not really feasible to have a single test that logs in, pokes around, and tests every feature. If something, ya know, 10 pages in breaks, you’ll have to wait an ungodly 60 seconds to verify your test works after every change. You need every test to start with a fresh browser and a fresh login.

OKAY, so you’ve got your end-to-end test, and it’s failing. Sweet. The next thing you want to do is work your way inwards from the point-of-contacts, writing unit tests and implementations until the end-to-end test passes.

Unit tests

The first thing the user sees is the login page/button, so I’d probably start with a failing test for that. Here’s a unit test for login taken from that same project:

Okay, so, yeah, lots more stuff going on here.

First, kind of glaringly, I’m using tape instead of mocha. I highly recommend it. It enforces a simplified testing structure, and moves away from the magic of mocha and chai and their hundreds of plugins. As Eric Elliot puts it, experience testing zen :)

Another thing to note here is the structure. Eric Elliott (am I a fan girl yet?) says every unit test should answer five questions, and this test very clearly answers all five:

  • The component I’m testing is in the name of the test
  • The component aspect I’m testing is in the message
  • How the component is tested is very clear in the second stanza of the test
  • The expected result is clear
  • And the actual result is clear

I like to put expected before actual (contrary to most JS testing libraries) because it follows TDD more closely: you’d think about the expected value first, before actually writing the implementation and getting the actual value out.

Also, I like to break tests into three stanzas: the first stanza is setup, the second is how the test is executed, and the third is the expected/actual values, and follow this consistently. This makes it very easily to parse tests at a glance and find out whats going on. It also tells you, if any of your stanzas are super long, when it’s time to break out setup into a separate function, or when your component is too difficult to test and needs to be refactored.

Testing the inputs and outputs of components

It’s important, I think, to focus purely on input/output in unit tests.

Test inputs and outputs of components. Don’t “reach in” and test the implementation.

Enzyme lets you “reach in” and grab the actual state of the component, or the instance methods. Using these features is asking for brittle tests, because they lead you to testing the implementation and not the actual functionality or behavior of the component. The component could remain exactly the same from the point-of-view of the application, but if change the state or instance method (the implementation), the test will fail and you’ll have lots of fun maintaining your app 😉

In React, the input is generally one of three things:

  • Static props
  • Events generated by the user
  • A child component calling a passed-in function

The outputs in React tend to only be three things:

  • The presence of a child component
  • A call to function prop, like a redux action creator
  • Values passed into child component props

So your setup/test should generally only reference the inputs, and your assertions should only check the outputs.

This keeps the component a “black box”, where your only test for effects. This makes your components wayyyy more flexible and lets you change the implementation without breaking a million tests, leading to lower maintenance costs.

Okay so you’ve got a failing unit test. Next you’d write the actual component, getting the unit test to pass. You’d slowly write failing unit tests, testing inputs/outputs, until you get to the edges where you need to send or get data from an outside source. This is where the interesting stuff happens 😉

Integration tests

I write integration tests last, at the edges of where my React application meets the outside world, which is generally a REST API. Here’s a failing test for an async redux action creator:

This test assumes you’ve got all of your redux-related files in a single `redux` folder (like they should be!), and the configureStore configures the store with redux-thunk middleware.

getUsers returns an async function that fetches from the api, and dispatches an action that gets processed by an entities reducer (from the normalizr library).

Sweet! An integration test. You don’t need to get super specific and write a ton of these. You generally want to follow the test pyramid:

Now, this pyramid confused me at first because, to me, it implies you’d write unit tests first, integration tests second, and acceptance (or end-to-end) tests last.

But really, a better process to follow I think is writing end-to-end tests first, unit tests second, and integration tests last :)

SO, you write your end-to-end test, slowly work your way through the unit tests to the edges where you write your integration tests, all the while filling out the implementations. Eventually, your end-to-end test passes and you’ve got yourself a sweg new, well-tested feature to deliver to the product team 😁

Where to put tests?

Recently, Dan Abramov tweeted:

I’d have to agree. Maintaining two parallel structures (one for source and another for tests) is a senseless task. I try to co-locate related concepts as much as possible, and that includes putting tests next to source (which is sooooooo nice now that I’ve started using it).

I like the Facebook convention of putting tests in a __tests__ folder. You can check it out in the React repo.

Facebook, though, doesn’t seem to differentiate between unit and integration tests, they just suffix each test with `-test.js`. I’ve been suffixing unit tests with `.unit.js` and integration tests with `.integration.js`. This is important because unit tests need to run super fast, and should be run in a pre-commit git hook (or at least on your continuous integration server). Integration tests are much slower because they have to fetch external resources, and should be run less frequently.

SO, I’ll suffix the tests and then if I want to run only my unit tests, I can type: `tape ‘src/**/*.unit.js’`, and if I want to only run the integration tests I can type: `tape ‘src/**/*.integration.js’`

Other tools

There are a few other tools that can support your testing goals.

First up is code coverage. It basically tells you what percentage of lines of your code is covered by the tests. I use nyc and babel-plugin-istanbul. Have your tests fail if coverage falls below a certain threshold. I think the nyc default of 90% is sensible — shoot for 100%, but if some code is truly difficult to test (it happens) that’s ok.

One realllllly cool tool you can use if you’re using Atom is atom-lcov. This shows you in the gutter, in real-time, what lines are covered by your tests using red, green, and yellow dots:

so you can easily shoot for covering 100% 🙂

Another tool you can use is a cyclomatic complexity checker. Cyclomatic complexity is a measure of how complex a function is. eslint has a complexity rule that will fail if your functions pass a certain threshold.

That brings me to eslint. I like to fail the build on the CI server if the eslint check fails. I use the Airbnb eslint config as a baseline, and relax it as needed.

Static analysis is also a good tool to use. It basically gives you statistical measurements that point to how maintainable your code is, and let you single-out problematic modules ripe for refactoring. plato seems pretty cool, but haven’t had a chance to use it in any projects yet.

Finally, testing and static analysis is great, but it only catches a specific class of errors (much like spell-checkers can’t test for grammatical errors). I highly highly recommend adding code reviews into your development process. In projects that I work on, nothing hits staging before someone else has reviewed the code. Also, like Code Complete said, code reviews quickly bring everyone on the team up to the level of the most skilled coder, and let teams hash out coding standards that fit the project.

So there you have it! The practices I use for testing React apps 🙂

Follow me on Twitter @TuckerConnelly

--

--

Responses (5)