React End-To-End Testing: A Guide to Getting Started

As a developer, I want to test the software I write. I'm constantly trading time spent testing software for time…

Testim
By Testim,

As a developer, I want to test the software I write. I’m constantly trading time spent testing software for time spent coding. It’s a struggle to know how much time I should spend testing a new feature to ensure it works. It’s also a struggle to know just which tests to write. When I’m writing React, knowing whether to test a component or perform a full end-to-end test is a struggle. Many testing guides focus on the former kind of testing, where you test just a component. But React end-to-end testing provides a lot of value and shouldn’t be ignored. Writing end-to-end tests can also feel like a real chore, especially when you’re starting. In this post, we’re going to talk about getting started with React end-to-end testing and some tools that will make writing those tests much simpler.

End-To-End Tests the Hard Way

The point of an end-to-end test is pretty self-explanatory. As the developer, you write a test that manipulates something on the UI, the application takes action, and a result is returned. The application, both UI and backend, process that action, then the test verifies that the result of that action is the same as you expected. As such, writing end-to-end tests requires a tool that will parse HTML and JavaScript and allow you to interact with page elements. Most people initially reach for a tool, but rendering React components in JavaScript is easier than many people anticipate. Running Chrome without opening a window is actually pretty simple. The same is true for browsers like Firefox.

The Simplest Test Script

Once you have your headless browser set up, writing a testing script is easy. For instance, let’s say that you want to test your login flow. This is a pretty common practice. After all, if your users can’t log in, they’re not going to be doing a whole lot else with your product. Writing this test script is actually pretty simple. Let’s say that we’re using the headless Chrome browser. Let’s walk through some example code to see how we could perform an end-to-end test on a local browser.

import { launch } from 'chrome-launcher';
import CDP from 'chrome-remote-interface';
import assert from 'assert';

(async function() {
  async function launchChrome() {
    return await launch({
      chromeFlags: [
        '--disable-gpu',
        '--headless'
      ]
    });
  }
  const chrome = await launchChrome();
  const protocol = await CDP({
    port: chrome.port
  });

  const login_flow = `document.querySelector('#username').value = '[email protected]';
document.querySelector('#password').value = 'test-password1!';
document.querySelector('#login-button').click();`

  Page.loadEventFired(async() => {
      const result = await Runtime.evaluate({
        expression = login_flow
      });
  });
  // Test something about the page after you've logged in here
})();

Assuming your home page is running on your localhost on port 3000, this test is your first step to performing an end-to-end test with React.

What the Test Script Does

This is a pretty simple script. We open a headless Chrome browser; then we tell it to load up http://localhost:3000. Once the page has loaded, we run a small JavaScript script within the browser. That script finds the element with the ID username and enters a test email address. Obviously, this is one you’d need to have set up in your local database ahead of time. Then, it steps over to the element with an ID password and enters a test password. Finally, it clicks on the element with the ID login-button. Then, it waits for whatever the page does and finally returns the result to you as an HTML string.

From here, you can do whatever you need to verify that the login operation was a success. Perhaps you want to ensure that the phrase “Welcome, Test User” appears on the page after the user logs in. You know that if you see that string, the login flow worked correctly. You might add a simple one-liner to test that you saw the string you expected in place of our comment up above. Something like this:

assert(result.includes('Welcome, Test User'));

Now, you can save this as a single script and run it if it returns successfully; congratulations! Your login page is working, and you’ve written an end-to-end test!

If you want to make this test more powerful, you might add one of the powerful JavaScript testing frameworks like Jest or Chai. These can parse the output of your test and allow you to make more complicated assertions. You can then tie multiple tests into a single test run to ensure your application’s logic and flow work the way you expect.

What the Test Script Doesn’t Do

Astute readers will notice that the test we wrote is pretty brittle. It relies on just a single browser (Chrome) and starts up the entire chrome process every time we want to run the script. That’s not very efficient! It would be best if you didn’t use this script to test your actual applications. Instead, we were using it as a way to explore the basics of end-to-end testing. Another issue here is that this test script won’t be easy to run automatically. If you were to plug this test script into a server running something like Jenkins, you’d need that server to have a full version of Chrome running on it. That’s a big dependency.

Instead of running a vanilla headless browser, many developers turn to a wide variety of browser automation frameworks. In fact, the test script that we wrote above is how many end-to-end testing frameworks originally started. Thankfully, we don’t have to relearn all those lessons by ourselves. We can use their time and hard work to make writing our tests easier and faster.

End-to-End Tests the Easy Way

Our current test is brittle, slow, and difficult to run in a continuous integration environment. While it does test what we want, it’s not up to our standards. Let’s take a look at how we can make this test much easier—and more flexible—by leveraging a tool like Testim’s automated test tool. We’ll adapt some test code provided in their documentation to rewrite the test that we were using before.

// Import Testim Dev Kit methods
const { go, waitForText, click, type, test, l, Locator } = require('testim');

// Load the smart locators data and make it available for use in this test
Locator.set(require('./locators/locators.js'));

describe('User Login', async () => {
  it('Logs in correctly', async () => {
    await go("http://localhost:3000");

    await type(l('#username', '[email protected]');
    await type(l('#password', 'test-password1!'));
    await click(l('#login-button'));
    const loginTitle = await waitForText(l('Welcome, Test User'));

    // Use Jest to assert the title text is correct
    expect(loginTitle).toEqual('Welcome, Test User');  
  });
});

This is a much cleaner syntax for the work that we’re doing. Instead of writing our steps into one string, which is difficult to read and not portable, we write it as a series of simple functions that the Testim Developer Kit (TDK) provides. The only major modification we’ve changed is to use Jest instead of Chai assertions. This kind of test is instantly readable for any developer who’s used to testing React components in modern JavaScript. It’s also fast and doesn’t rely on desktop browser executables to run. Each step is the same as the code we wrote above, but this code will run much more quickly due to not needing to start a new Chrome browser every time.

What’s more, because you’re not using a single string to represent your actions anymore, you can abstract the entering of your username and password step, then run multiple tests for login without needing to duplicate that code. For instance, what if you could also log in by pressing the Enter key inside the username and password fields? In our brittle test format, you’d need to write three different scripts for that action. With Testim’s developer kit, you can write one test that reuses the logic for entering the username and password, as well as the logic checking for your welcome message. Your tests are better, and you’re writing less code.

End-To-End Tests the Really Easy Way

While it’s useful as a developer to write code tests that exercise your app’s functionality, Testim can take that simplicity to a whole new level. Sometimes, you need tests that are designed by people who don’t know how to code. Some things are so simple (like our login example above) that writing code for it isn’t really worth the investment. Besides, many tests will take a long time to write as code, and when completed, small changes to the application could cause them to break. That’s where Testim test recording comes into play. Instead of writing an entire test, you can record your steps through an application, configure them with assertions/validations, variables, etc., and Testim will play them back as part of your continuous integration system. You’ll know that your most important application flows work, step-by-step, every time you push a commit.

As I stated at the top, testing often takes away from time spent coding. Thus, faster authoring and less maintenance of tests will free up my time to create new and innovative features—a fair tradeoff, in my opinion.

React end-to-end testing can feel intimidating when you’re starting, but it doesn’t have to. By leveraging a tool like Testim, you can start writing powerful, reusable end-to-end tests in just minutes. Even better, you can start using Testim for free. What are you waiting for?

What to read next

React Native Unit Testing: A Complete Getting Started Guide

Automated Testing Tools for 2020: The 11 Essential Ones