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.