The Guide to Test Automation of ReactJS Apps: Part 2

Contents

How to Deal With React Data Grid

Grids testing might be quite complicated, and QA engineers usually underestimate the potential testing effort. The main problem with grids is in the amount of data and its potential permutations.

You know that exhausting testing is impossible, so one of the key QA goals is to perform data analysis first and apply special testing techniques to reduce the number of inputs. Luckily, it’s out of this topic’s scope. 🙂 We’ll pay attention to more technical stuff.

So, you may be wondering, what are the most popular automation scenarios in the context of Data Grid? The answer is: sorting and filtering.

To effectively automate such cases, you should be aware of different pitfalls which may appear on the frontend as well as on the backend side.

Let’s take a look at a couple of popular issues, which may occur during testing. Then, we’ll try to perform root cause analysis, and understand how this information may help us in the further automation effort.

We will play with the same grid, which was described a little bit earlier, in Part 1.

Let’s say we want to check ASC sorting of the Age column.

Our sixth sense will likely whisper that 121 is a bit redundant, right? But how? Not quite obvious.

As QA Engineers, we have 2 options:

  1. File a bug and have a beer. Let’s leave it for Developers to handle.
  2. The alternative requires more time and brain involvement to perform at least a high-level root cause analysis.

As we are all Engineers (I hope), probably the second option will work better.

So the first thing we’d probably do is a browser’s Network tab analysis, right? Let’s check it.

And what do we see? The Users endpoint returns JSON, where the age field has a string type! Really?! It means that the backend returns the wrong data type for some reason.

So what would be our next step? Checking the model, of course!

public class User implements UserDetails, Serializable {

    private static final long serialVersionUID = 7449222326401403686L;

    @Id
    @Column(name = "user_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(max = 40)
    private String name;

    @NotBlank
    @Size(max = 4)
    private String age;
    //...
}

And here’s it, a root cause! Just look at the age field which has a String type. Nice catch!
Anything else we could do? Well, maybe it makes sense to double check a database? Let’s do it.

Look at this – varchar?! Fantastic! So we have put all things together. Most likely some Architect or Developer made a mistake while designing a database. Chances are that the other backend developer just generated an entity from DB schema, without any doubts regarding the column type mismatch. Why? “It was an Architect who had designed this DB,” he might have thought, “He definitely knows what to do!” On the other hand, the front-end Developer didn’t bother with JSON schema validation.

As a result, we’re faced with a chain of human mistakes and a bit of indifference, which caused a bug with a single Grid’s column.
Here’s another example. Now let’s play with filtering. The same grid, but the Salary column. We’ll filter the users whose salary is > 874.71.


In the combination with ASC sorting, the results seem valid.

As we’ve copied this value directly from the grid, it would be also nice to check the boundary, right? Let’s use >= operator and check if the corresponding record will appear on top of the grid.

Oops, a new operator, but old results? How is it possible? Let’s perform a root cause analysis!
Again, looking at the Network tab and checking the response:


Nice! The backend returns 8 digits after a period. And the UI displays only 2. Here it is. Now we are sure that the problem is on the frontend, right?

Let’s take a look at the Grid component. We are especially interested in the code, which is used for cells formatting.

export function floatFormatter(props) {
    return baseFormatter(props,
        val => isNumber(val)
            ? numeral(val).format('0,0.00')
            : val);
}

What a shame! We check if value is a number and just format it to the following view: 0,0.00.
And it’s easy to verify against UI. We just need to copy the full number, which comes from the backend, and paste it into the filter:

Nice catch, isn’t it? So what does such root cause analysis give QA engineers? First of all, now we know for sure, where the bug is stemming from. And what else? Apart from that, we can define a very straightforward set of issues to pay attention to while designing automated tests:

  • types mismatch
  • rounding issues
  • formatting issues

Of course, you can find out more during the detailed root cause analysis of any other issues. But I’ll leave it to you. 😉

The Balance Between Simplicity and Versatility

So what about the automation? It’s very important to keep the balance between simplicity and versatility, so that no matter what column you’re passing as input, your test will know exactly how it could be sorted, and moreover, how to check a sorting order depending on the types we’re working with.

@Test(dataProvider = "sortingData")
public void recordsShouldBeSortable(final Column column) {
    open(LoginPage.class)
        .loginWith(User.dummy())
        .sort(column, ASC);

    verifyThat(at(GridPage.class)).recordsAreSorted(column, ASC);

    at(GridPage.class)
        .sort(column, DESC);

    verifyThat(at(GridPage.class)).recordsAreSorted(column, DESC);
}

@DataSupplier
public StreamEx sortingData() {
    return StreamEx.of(AGE, SALARY);
}

The similar situation is with filtering. A test must be flexible enough to work with any column, operator and value type.

@Test(dataProvider = "filteringData")
public < T > void recordsShouldBeFiltered(final Column column, final Operator < T > operator, final T value) {
    open(LoginPage.class)
        .loginWith(User.dummy())
        .adjustColumn(column)
        .expandFilter()
        .filterBy(operator, value);

    verifyThat(at(GridPage.class))
        .recordsMatchCondition(column, operator, value);
}

@DataSupplier(transpose = true)
public StreamEx filteringData() {
    return StreamEx.of(SALARY, GREATER_OR_EQUAL, 874.71);
}

You may just be wondering, how we could achieve such conciseness and flexibility? In one of my Selenium Camp talks, I demonstrated the way of Selenium ExpectedConditions encapsulation behind simple enum values. This same technique could be applied to Grid columns:

@Getter
@RequiredArgsConstructor
public enum GridColumn implements Column {
    AGE(Column.TO_INT, NumericFilter.class),
        SALARY(Column.TO_DOUBLE, NumericFilter.class);

    private final Function < String, ? > valueMapper;
    private final Class < ? extends Filterable > filter;

    @SuppressWarnings("unchecked")
    public < T extends Filterable > T getFilter() {
        return (T) use(filter, this);
    }

    public String getName() {
        return name().toLowerCase();
    }
}

Tests or data providers may operate with some obvious names like AGE, SALARY, etc. But each column will know for sure which types it should work with, and how to convert Strings to the required data type. That’s very important since Selenium gives us Strings. And we can’t operate raw Strings in the context of such tricky columns as AGE and SALARY. Otherwise, we’ll replicate the same issue, which we’ve just uncovered on the backend. Moreover, we’ll hide the real bug of the application!

The situation with operators is a bit trickier as we have 2 different representations of the same thing on UI and in the code. For humans it’s obvious to use operators like >, <, >= or <=. But the machine won’t be able to understand it, as it’s just a symbol. We can’t say: “Hey, Java Stream, here’s a >= symbol, take care of everything else”. So the only valid option is to teach our program, what this sign means in terms of data processing. And it also could be implemented with a help of enums:

@Getter
@RequiredArgsConstructor
public enum DoubleOperator implements Operator<Double> {
    EQUAL("", Double::equals),
    GREATER(">", (ob1, ob2) -> ob1.compareTo(ob2) > 0),
    LESS("<", (ob1, ob2) -> ob1.compareTo(ob2) < 0),
    GREATER_OR_EQUAL(">=", (ob1, ob2) -> ob1.compareTo(ob2) >= 0),
    LESS_OR_EQUAL("<=", (ob1, ob2) -> ob1.compareTo(ob2) <= 0);

    private final String value;
    private final BiPredicate<Double, Double> filter;
}

Such mappings allow keeping our code clean and flexible so that we could easily integrate a complicated data processing logic directly into the streams or assertions by request.

And don’t forget about the root cause analysis! It opens up some great new opportunities in terms of testing.

Charts Automation Principles

Our last part, the most complicated one, is dedicated to Charts. You may be wondering why the most complicated? Simply because you won’t find any useful information about Charts automation anywhere on the Internet. And you perfectly well know that people’s laziness already reached to the point when if there’s no detailed instruction or video found, they are just stuck without any chance to recover.

So what about the automation? The key question you should ask yourself before starting any charts automation effort is: how do they render?

Currently, there are 2 the most popular formats: Canvas and SVG. So if your charts are rendering as Canvas, I have some bad news for you. Canvas is just an image. And images could be automated either by means of screenshots comparison or using specialized image recognition tools like OpenCV or SikuliX. But I don’t believe you want to go down this fragile path. The best choice for automation is SVG. Simply because SVG integrates directly into the DOM, and could be easily accessed via the common Selenium API.

Ok, that is clear. And what can we test on the charts? I would highlight the following points:

  • Data. That’s pretty straightforward if your charts reflect e.g. some table data, which you have access to. But sometimes data can be completely unmanageable in terms of the amount and external sources. So the latter case will severely restrict your testing to some edge cases, e.g. data anomalies detection.
  • Business logic. This is a very domain-specific item. During one of my previous projects, we had 80% of features based on Grids and Charts. So you can imagine the variety of complex functionality which had to be tested.
  • Visual elements. It is a subset of the previous item. It depends on whether you are using out-of-the-box charts’ features or some visual customization. The latter should definitely be tested.

Now let’s see some interesting examples of how we can automate react-stockcharts.

This chart is a bit customized and rendered in the mixed (Canvas + SVG) mode. If we hover over any point, the following popup appears.

Let’s consider the following business requirements for further verification:

  • Charts points should be located in the chronological order.
  • The color of the popup border depends on the difference between open and close. The positive value should lead to the blue border. Otherwise, the popup should be red.

That seems quite obvious and someone may wonder if it’s reasonable to automate such cases at all. But from my experience, tricky bugs may appear even with such simple flows.

Let’s say, if the frontend doesn’t perform any data pre-processing and renders everything as is, you’d be very surprised to see a “reverted” chart when the backend returns data in the DESC order. Or how would the second rule work in the case if open == close? Which color should be displayed? 🙂

To automate such scenarios we need to define an algorithm first. And you’ll definitely be astonished to find out how easy it is. There are only 2 simple actions involved:

  1. Moving cursor over some point on the chart.
  2. Extracting some information from the popup for further analysis.

So basically, we need to hover over each point (which is called a candle for this particular chart type), and extract the text from the popup.

Both actions are pretty common for Selenium. Moreover, almost every test requires some data extraction from UI. Is it that simple in the case with a mouse move? To answer this question, let’s first think about what inputs we need to handle within such a task?

Probably, we will start with the chart width and height to define the restrictions, right? What else? We’d definitely need [x; y] coordinates to perform a movement. And if y could be a fixed value, due to the way the chart renders a tooltip (popup will be displayed at any point), x is a bit trickier. We can’t just increment its value on each iteration. The chart is a set of pixels, and each candle has a fixed width, which is > 1. So our main goal is to find the width of a candle, which would be an x offset for each iteration. This could be done via an online ruler browser extension.

Ok, now we know exactly what to do. Let’s see how it can be automated.

@Step("Scan chart and retrieve tooltips info.")
public ChartPage scan() {
    val chart = $("rect.react-stockcharts-crosshair-cursor");
    val width = parseInt(chart.getAttribute("width"));
    val height = parseInt(chart.getAttribute("height")) / 2;

    StreamEx.iterate(0, x - > x + CONFIG.movementStep())
        .takeWhile(x - > x <= width)
        .forEach(x - > moveTo(chart, x, height).readTooltipContent());

    return this;
}

After receiving the required inputs, we can use an infinite stream to update x coordinate, before reaching the right border (chart width). On each iteration, the two actions described above are performed: moving the cursor and reading the tooltip content.

private ChartPage moveTo(final SelenideElement element, final int x, final int y) {
    actions().moveToElement(element.getWrappedElement(), x, y).perform();
    return this;
}

private ChartPage readTooltipContent() {
    val date = parse($("tspan[data-qa=time]").text(), ofPattern("yyyy-MM-dd"));
    val open = parseDouble($("tspan[data-qa=open]").text());
    val high = parseDouble($("tspan[data-qa=high]").text());
    val low = parseDouble($("tspan[data-qa=low]").text());
    val close = parseDouble($("tspan[data-qa=close]").text());
    val color = $("g.react-stockcharts-tooltip-content>rect").getAttribute("stroke");

    tooltipContentList.add(new TooltipContent(date, open, high, low, close, color));

    return this;
}

Note that it’s better to parse tooltip data into the entity and save it into some collection for more flexible post-processing on verification steps.

@Data
public class TooltipContent {

    private final LocalDate date;
    private final double open;
    private final double high;
    private final double low;
    private final double close;
    private final String color;

    public boolean hasColor(final StrokeColor color) {
        return Color.fromString(this.color).equals(color.getValue());
    }

    public boolean hasPositiveEODValue() {
        return open - close >= 0.0;
    }
}

The entity may also encapsulate additional logic for color rules processing. Let’s see how it can be used on the assertions level.

@Step("Verify that tooltip dates are in chronological order.")
public ChartAssert tooltipDatesAreInChronologicalOrder() {
    isNotNull();
    val actualDates = StreamEx.of(actual.getTooltipContentList())
        .map(TooltipContent::getDate)
        .toList();
    val expectedDates = StreamEx.of(actualDates)
        .sorted(LocalDate::compareTo)
        .toArray(LocalDate.class);
    Iterables.instance().assertContainsExactly(info, actualDates, expectedDates);
    return this;
}

@Step("Verify that tooltip colors match EOD price diff rules.")
public ChartAssert tooltipColorsMatchEODPriceDiffRules() {
    isNotNull();
    Iterables.instance().assertAllMatch(info, actual.getTooltipContentList(),
        tooltip - > tooltip.hasPositiveEODValue() ? tooltip.hasColor(BLUE) : tooltip.hasColor(RED),
        new PredicateDescription("Tooltip colors match EOD price diff"));
    return this;
}

The first verification scenario is pretty straightforward, as checking the chronological order could be treated as a classic sorting task.

Checking the color rules for tooltips is a bit tricker, as we have to define a custom predicate, which “asks” 2 questions:

  • is the difference between open and close positive?
  • depending on the first answer, we may ask a second question: which color does a tooltip have?

These 2 answers should be applied for each tooltip. And if all of them match specified criteria, a test will pass.

@Test
public void chartPointsShouldMatchDateAndColorRules() {
    open(LoginPage.class)
        .loginWith(User.dummy())
        .select(ChartPage.class)
        .scan();

    verifyThat(at(ChartPage.class))
        .tooltipColorsMatchEODPriceDiffRules()
        .tooltipDatesAreInChronologicalOrder();
}

As you can see, the body of the test is very concise and readable.

Was it too complicated? Not really, right? But this simplicity can be achieved only if you follow some basic conventions regarding data-qa attributes (as the charts components like tooltips are not quite fancy in terms of the DOM structure), and your charts are rendered in the SVG format.

If you’re still wondering whether it is reasonable or not to automate such scenarios, remember how much time you’re wasting on each testing phase by checking all these tricky moments manually. Wouldn’t it be better to delegate such tasks to a machine? 😉

Summary

  • Learning your product’s internals will open absolutely new opportunities in terms of testing and automation. You’ll get a better understanding of the nature of issues, which will definitely help you to prepare a more concise automation strategy.
  • In terms of internal communications, always prefer negotiations over workarounds. Don’t be scared of asking Developers to add some custom attributes or open backdoors if it can drastically simplify the overall testing effort.
  • Invest your time in learning programming languages. Only a strong technical background will help you to evolve and adapt to any IT environment changes in the future.

That’s pretty much it. Feel free to ask any questions. As always, you can find a full source code on my GitHub.