The Guide to Test Automation of ReactJS Apps: Part 1

Contents:

Introduction

A modern Software Engineer in Test must combine the skills and mindset of a software developer, QA engineer, and DevOps engineer. The tendency for technology growth shows that in the nearest future the Engineer’s “survival” will drastically depend on the ability to evolve and adapt to the rapid IT environment changes.

Being a conference speaker, I always insist that QA Engineers should learn programming languages, as well as the internals of the software they test. In this article, I’m going to dwell upon the benefits this knowledge may potentially bring, and how it might improve the entire software testing and automation process.

But before the actual discussion about applications testing, we have to define the context first. You may have already guessed that we’ll be talking about ReactJS applications. React was selected because at present it’s one of the most popular libraries for building user interfaces.

Let’s start with a set of definitions and then we will drill into details.

The Basics About ReactJS

In React, User Interface is just a tree of components. A component is the main structural unit, which could be represented by something very small, like a button, input or label, or by something more complex, like a registration form, user profile, etc. Generally speaking, any element could be a component.

All components are encapsulated, which means that we can’t access their internal API from the outside. Only so-called properties (or just props) can be exposed to the outer world for further manipulation.

Components may have a state. But it’s not mandatory. React supplies an out-of-the-box mechanism called local state. But unfortunately, it’s not sufficient for the real world applications. That’s why developers prefer using some 3rd-party libraries like Redux or MobX for more flexible state management. My further materials will be based on React + MobX chain.

The state of components usually mutates when some events are triggered. With web applications testing, we got used to some classic mouse or keyboard events. But it’s important to note that not only UI but also backend events may affect the state of components.

Software Engineers in Test are already familiar with Document Object Model (DOM). Especially, in the context of web automation. So what is a Virtual DOM? For now, we may think of it as of a very lightweight version of a real DOM. But if you really want to know the details, just read the official docs.

Let’s check how all these keychains work together in React.

We already defined Component as the main structural unit in React. But components are usually grouped into logical modules.

A state of such modules is usually split from the components and moved into so-called Stores.

One of the primary goals of a state management tool is to track and react to the updates of the state of components. And that’s where events come into play.

Let’s imagine a classic Registration form, which could be treated as a logical module, or just as a set of components.

A user types something into the Username field that automatically triggers a related UI event, client validation, and as a result – the mutation of the component state.

We can then push a Tab key, triggering another event – onBlur. Username verification request is sent to the backend, and again, we affect the component’s state.

So that’s a basic idea of how different events could potentially change the state of components.

When a state management tool detects changes, a special render function is called on a component.

Note that the component is not immediately re-rendered. And that’s where Virtual DOM (VDOM) appears. When the rendering procedure is initiated, an actual component’s VDOM snapshot is taken. Then changes are applied, followed by another VDOM snapshot. When we have 2 VDOM snapshots, it’s time to check the difference between them. For that, React uses a special heuristic algorithm called reconciliation. When it finds the difference, it could be transformed into a real DOM and the final result that the user sees on UI.

It’s important to understand this concept. It is to the VDOM that React owes the ability to build high-performance applications.

Test Automation Challenges in ReactJS

Now let’s try to determine what this knowledge means to us, QA Engineers. Well, it brings a very straightforward set of automation challenges:

  • Locators lookup. Sometimes it’s quite hard to find and interact with React components. And it’s not only related to their encapsulated nature. Rendering specifics leads to the point where there could be lots of identical components located within the same page. And the main question is – how to deal with it?
  • Wait context. From time to time, it’s not quite obvious what components we should wait for, and what conditions should be met before the actual interaction. A very common case in React is when we rely on some root element’s visibility, and can’t even predict that its children may constantly be updated by different events without touching their parent.
  • Nodes staleness. Software Engineers in Test who work with web know what StaleElementReferenceException is. In React, you can see it often due to components’ rendering specifics. Especially, while working with a collection of WebElements.
  • Events triggering. Sometimes, it’s required to interact with components directly via Javascript. Datepicker is a good example when we don’t really want to interact with its calendar via UI. Usually, API usage is the best choice, but we already know that it’s impossible to access React components’ API. So events triggering could be another option for us. As there’s no easy way to dynamically attach a required event to DOM node, represented by React component.
  • Rendering frequency. VDOM concept allows UI re-rendering with a high rate. For one of the applications we’ve built not so long ago, there was a requirement: data on UI should be updated with 4 times per sec rate. And it’s not a limit for React. So which modern test automation framework could read data from DOM with such frequency (assuming remote execution via Grid or similar solution)?

If you’re still here, and didn’t give up, thinking that React is something completely unmanageable in terms of automation, keep reading… 🙂 As of course it’s not that bad as I’ve just described.

Team Communication As a Way to Make It Simpler

Let’s see how React automation processes can be drastically simplified. You may think of some dark magic involved to make things easier. But that’s not really how it works.

The one direction you should be looking at is communication within your team. Yes, however obvious, it is very important.

I’ll give you an example of conventions we’ve built in our teams between Developers and QAs:

  • If Software Engineers in Test encounter any difficulties locating or interacting with some components, they either create a Jira ticket (the preferred way) or just ask Developers directly to add a unique attribute to the target component. Let’s call this attribute data-qa. You know perfectly well that the more concise the element’s locator is, the less maintenance effort is required in the future.
  • The events for React components can be triggered via a special “backdoor” called ReactTestUtils, which can be exposed on the browser level. At least for Development or Testing environment. Adding such backdoor may significantly reduce the automation effort interacting with components like Datepicker.

Is it hard to follow these conventions from the Developer’s perspective?
Imagine we have the following grid:

Now let’s check a DOM tree for some header / common cell:

As you can see, both cells have a class attribute. But it’s useless in terms of test automation, as all other cells have exactly the same attributes. So it won’t be possible to create some flexible locators without the explicit usage of text or indexes.

Let’s see how to add a custom data-qa attribute to a Grid component.

In general, Data Grid supports custom formatter and header definitions. To override the header row and common cells’ behavior we can do the following:

.map(column => ({
            ...column,
            headerRenderer: <div data-qa={column.key}>{column.name}</div>,
            formatter: extendedFormatter(cellFormatter, { 'data-qa': `cell-${column.key}` })
        }));

Header cells are wrapped with data-qa attribute via headerRenderer. And to do the same for common cells, we can define a custom cell formatter. You can find more details in a sample Grid component’s sources.

As a result, 2 simple code lines could be transformed into the following layout:

Now we can easily locate the required cells by the newly created data-qa attributes. For example, for Age column values retrieval, we can type the following:

So for the Developer, these conventions come down to adding some custom attribute. Just think of all the benefits this approach brings to Software Engineers in Test.

Ok, that’s clear. What about the events? Let’s see how it goes with the following Datepicker component.

When we pick some date from a calendar, the following debug message is generated in the console log:

It notifies that onChange event is triggered to update a date value. Let’s remember this fact.

What we’re going to try next is to do the same without UI, via pure Javascript.

Here we locate an element via custom data-qa attribute, set a new value, and check the UI:

The value seems to be updated. But we still see a weird error about the user’s age. And it makes no sense as the year 1989 shouldn’t fall under the field’s restriction.

To understand what happened, let’s take a look at our schema again:

As you may have noticed, there was no debug message generated in the console log. It means that the component’s state hasn’t been mutated while setting the date directly via Javascript. As we haven’t affected the state, the component still thinks that there’s an old value present. That’s why an error message remains even with a new date.

Now we know that only onChange event triggering should update the corresponding value in a SignupStore. You may be wondering how to do that? 🙂 The answer is – ReactTestUtils. Let’s create a backdoor to see this object in a web browser.

import ReactTestUtils from 'react-dom/test-utils';

window.ReactTestUtils = ReactTestUtils;

Generally speaking, ReactTestUtils were created exactly for the testing of React components. So it’s a shame to ignore it for high-level UI testing. 🙂

To use it in the web browser and Selenium scripts we have to set its reference into a root window object.

Calling it from browser console should give us the following:

As you can see, there are lots of useful APIs for ReactJS testing. But for now, we’re interested in Simulate. Let’s see what it allows us to simulate:

Events! Nice. So all we need is to call the required event and pass our DatePicker DOM node as a parameter:

Wow, that was easy! And no more errors on UI.

Summary

Bringing such kind of conventions to the team may greatly reduce the automation effort in terms of locators lookup and their further maintenance. If we talk about complex elements automation, like DatePicker, it’s important to remember that it’s a 3rd-party component. So it makes no sense to test it. As it’s already been tested by the component’s contributors. What we really want to do is to bypass some value via internal API. And that’s where ReactTestUtils may help. But of course, you should keep in mind that it’s not recommended to backdoor it on the Production environment to avoid security risks.

That’s it for now. Part Two of our Guide will come out shortly and here’s what it will consist of:

  • How to Deal with React Data Grid
  • React Data Grid: The Balance Between Simplicity and Versatility
  • Charts Automation Principles

If you have any questions or need help with test automation for your ReactJS applications, feel free to reach us at info@waverleysoftware.com. Stay tuned!