Flaky Tests, and How to Deal with Them
Tags: webdev, flakytests, javascript, testing
Published: 2023-01-26 15:45:35


![Battling Flakiness](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nf69kaxw58c98p40udb1.jpg)

## Intro
Hey! My name is Yarden Porat, and in this article, I will explain what flaky tests are, their costs, their causes, and how they harm your work and organization. Once we have that figured out, I will share our strategy and tools we have developed in-house for dealing with test flakiness at Wix and how we avoid their costs.

## What is a flaky test?
A flaky test is an automated test with a non-deterministic result. This is a way of saying that a test sometimes passes and sometimes doesn’t, inconsistently, without any code changes.


![failing test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/amfvyjc1e5qrqzxa9yxi.png)


## How often does it fail?

Beautifully depicted in this [article](https://shopify.engineering/unreasonable-effectiveness-test-retries-android-monorepo-case-study), if a single test has a failure rate of 0.05% (0.0005), and you have 100 of these tests in your test suite, it would have a success rate of 95.12%= 0.9995<sup>100</sup>.

But what happens when you have thousands of these tests?  A 60.64% success rate (0.9995<sup>1,000</sup>). It’s easy to calculate the significant impact of even a low failure rate on large scale tested applications.

**But… What’s the problem? Just rerun the tests!**

There are some really bad implications of ignoring flaky tests. Let's go over some of the most common ones, from least important to most.


### 1. Wasted CI minutes (Hours? Days? Weeks?)

Consider the following scenario: 

You are a developer working in a team. There’s a new feature you’ve been developing for several days, and you opened a pull request wanting to merge it into the project. 

Now, your company works in a modern development workflow and runs automated tests on your code changes using your CI system. All of the product’s tests ran and failed on a test **entirely unrelated** to the changes you introduced. 

Since you are **aware** of the code changes you have made, and you **know** that this project has issues with non-deterministic tests, you therefore **know** the failing test is not your fault.

So, you rerun the test, and it passes.

If you don’t know your project has an issue with non-deterministic tests, you’ll probably waste even more time investigating.


![rerun failed jobs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/om320bhbwcin7zkwnsa8.png)


The problem is that this time accumulates. The longer the test workflows take, the more time is wasted—but how much time? This can be measured, assuming you track your CI test results. 

You can easily calculate the CI time wasted due to flakiness by summing up the CI time of a workflow run that had a non-deterministic result. For example:




| #  | commit| workflow_name  | os | result | duration
| ------------- | ------------- | ------------- | ------------- | ------------- | ------------- |
| 1  | 9f3e679  | test-part-1  | linux  | success | 10
| 2  | 9f3e679  | test-part-1  | linux  | fail | 7

Run identifier: commit + workflow_name + os

That’s 7 minutes of CI time wasted!


### 2. Wasted development time 

When a developer reruns a test, they are forced to wait (again) for the build and test time. Precious developer time is being lost.

Even if we assume that a developer utilizes this wait time for other tasks, we still have 2 major drawbacks:
Loss of immediate feedback (long feedback loop).
Context switching—which eats away at focus and productivity.

Unfortunately, this wasted time is much harder to measure.


###  3. Flaky product behavior (or flaky implementation)

Sometimes a flaky test is only a symptom of a non-deterministic implementation.  

The same race condition that can cause test flakiness can do the same in a feature’s implementation, thus causing flaky product behavior in production. 


###  4. Alert fatigue

A common phenomenon in flaky test workflow is the loss of trust in the feedback you are getting. Consider this scenario:

1. You push your new code
2. Workflow tests run and fail
3. “Oh it's that annoying flakiness again; we should fix it sometime”
4. Rerun workflow, tests run and fail
5. “D*%N FLAKINESS”
6. Rerun workflow, tests run and fail
7. Realizing that it was actually my code changes that had failed the tests
8. Go back to step 1

![multiple reruns](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gwfjgm4bcfdk9k5ovy2j.png)

This harms development velocity and the developer’s experience. In an environment where it's not mandatory for tests to pass to merge a pull request, it is not uncommon for changes to merge even though they are breaking some product and tested behavior.

### What's lost?

- Money (Developer time, CI time)
- Development velocity
- Confidence in tests (regressing to manual testing)
- Product quality
- Developer experience


## Causes of test flakiness
So now that we know the price and the pain, here are some of the causes of test flakiness.


### 1. Poorly written test code
For example, interacting with DOM elements that are not yet ready, or improper use of `waitFor` functions. This is the most common case where testing is done incorrectly. Sometimes, powerful development machines (a.k.a, your local computer) hide race conditions in a test, which ends up failing on CI machines.

### 2. Poorly written application code
As mentioned above, sometimes the application code itself introduces a flaky behavior. These cases are much harder to detect and debug. It could be related to communications, asynchronous code, or many other alternatives.

### 3. Infrastructural causes
There are various environmental causes to blame, and they are the immediate culprit for those who write flaky tests. Such causes may be:

- Network issues: loss of connectivity, slow connection, etc.
- Hardware issues: low-performance shared virtual machines, which stress existing race conditions
- External dependencies: package manager (npm\yarn), runtime setup (i.e. Node, and other dependencies, which also suffer from some level of flakiness

### 4. Test tools that are prone to flakiness
In our experience tests which use a browser are more prone to flakiness. One reason is that the browser itself is a complex piece of software with many dependencies, and it can be affected by a variety of factors - its version, operating system, and other specific configurations of the machine it is running on.  

## Key takeaways up to this point
Here are some points I think you should keep in mind:

- Flaky tests could occur due to many reasons and various causes. Some are test related, some production-code related, others from the infrastructure and development environment.
- They have direct and indirect implications on the development process—both technical and psychological.
- Flaky tests reduce development speed and quality if left untreated.

## How to deal with flaky tests?

### 1. Collect data
It is much easier to communicate the costs of flakiness to your team or organization if you have data to back you up.


![Upload test results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fihwlb63xp7nvhhz59s3.png)

## 2. Analyze it
### Workflow reruns per day bar graph

A bar graph that represents the overall flakiness and displays the total number of times a workflow has been restarted.
It helps us understand the scale of the flakiness problem and the lack of developer trust in the tests/CI.

At [Codux](https://www.codux.com/) we chose to count any case of workflow rerun, but you can also create a subset of this graph that shows reruns that **never succeeded**, which could better depict the lack of trust in your tests/CI.

This is a general index that tells if your data correlates with your **general feel** of flakiness. We don’t derive tasks from it.

![Reruns per day bay graph](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/thmtj66xghc1074nxpw9.png)

- We count rerun by identifying the commit, branch, OS, and workflow name. We call it an “entity” and count its total occurrences minus one.



### Fail rate table

This is a table that calculates a test’s fail rate out of its total runs. We collect data from all branches, including development branches, and present only tests that have failed on 3 branches or more, with a minimum number of total runs.

This table helps us find the current culprits tests. A Flaky test that fails over an arbitrary percentage of your choice (we chose 5%), is skipped, documented, and assigned to the relevant developer. This process occurs 1-2 times a week.

This process requires reasoning and shouldn’t, in our opinion, be done automatically — for example:

- Some features have a low number of tests, so you probably wouldn’t want to lose coverage, and you might prefer, or should, add a retry on those tests.
- Some tests are more prone to failure (during development), such as end-to-end tests, so it might indicate they have a higher fail rate than they actually do.

![Fail rate table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s27grryy46r8nxl57bj5.png)

### Fail by test scatter plot (Environmental factors)

We’ve created a plot similar to [Spotify’s Odeneye](https://engineering.atspotify.com/2019/11/test-flakiness-methods-for-identifying-and-dealing-with-flaky-tests/#:~:text=I%20discussed%20below%3A-,Odeneye,-Odeneye%20is%20a). This plot helps us realize if there are some environmental or infrastructural problems. If you suspect your infrastructure is causing flakiness, try creating this dashboard.

![Fail by test scatter plot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qkvptyayokx6hi43y7xu.png)
_
Horizontal lines indicate that a test is flaky. Vertical lines indicate an issue external to the test because it shows multiple test failures in the same timeframe. _


### 3. Run new tests multiple times
After noticing that newly created tests are flaky and require adjustments, we have decided to raise the bar for newly created tests and created “**check-new-flaky**” — a CLI tool that detects new tests and runs them multiple times.

It detects new tests by running our test runner (mocha) programmatically, recursively extracting test names on the branch, and comparing them to master.

Checking newly created tests reduced the new flaky tests added to the application and the need to refactor them significantly.

![Run new tests multiple times](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5tid52e9w23tajv09s2e.png)

#### Some more benefits that we got:

- **Faster feedback loop**: This test workflow runs your new tests immediately, thus letting you know if it passes without waiting for their turn within the entire test suite
- **Another OS is running your test**: All our tests are running on Linux, while tests/features which are considered to be operating system sensitive, also run on Windows. Using the check-new-flaky CLI, we sometimes get an indication that a test we thought wasn’t OS sensitive is actually sensitive or broken for the other operating system.

![New flaky test shall not pass](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ym92rmitg4yz8vqzn7t1.jpeg)

### 4. Set a bar for when a test isn’t flaky
At first, it wasn't really clear to a developer when he fixed a flaky test. Developers would usually run a test 2-10 times before it would be labeled as not flaky and get merged to master.

Once we declared war on test flakiness, the bar would be set to 100 consecutive runs.

There are many ways to run a test multiple times — we used parts from the above CLI (check-new-flaky) and made it accessible via our GitHub bot.

![Benchmarking a test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2c308lm427dqanwswma6.png)

## Does your test only fail when running on CI?

CI machines usually have reduced performance compared to your local development machine, thus most race conditions only show once tests are running on the CI.

### Helping tests fail on your local machine

One tool that we have found to be helpful is CPU throttling emulation. 
We use [Playwright](https://playwright.dev/) for integration and end-to-end browser tests. It emulates slow CPUs using the [Chrome Devtools Protocol](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setCPUThrottlingRate) (experimental feature). 

```ts
  import type { ChromiumBrowserContext } from 'playwright-core';
   ...
   const client = await (page.context() as ChromiumBrowserContext).newCDPSession(page);
   await client.send('Emulation.setCPUThrottlingRate', { rate: 2 });

```
_Rate is the slowdown factor (1 is no throttle, 2 is 2x slowdown)_


### Find out what’s going on with a test on the CI

Many testing tools today allow you to take some recordings of your tests. 
Playwright released a [tracing feature](https://playwright.dev/docs/trace-viewer/) on version 1.12, which records the test flow and provides us with screenshots and DOM snapshots. Since we had a significant issue with flaky tests, we immediately integrated this feature into our testing utils, allowing developers to record runs.

We send CI tracing to a dedicated Slack channel for ease of use.

This feature is super helpful when you have no clue why the test is failing on CI. Tracing helped us catch some unimaginable bugs that we wouldn't have caught otherwise.

![Send CI trace files to slack](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vkg4mobvpfzt90v3tcsm.png)



## Stop using ElementHandles. Start using Playwright Locators

Following the release of [Playwright Locators](https://playwright.dev/docs/locators) and [ElementHandle being discouraged](https://playwright.dev/docs/locators#locator-vs-elementhandle) from use, we decided to migrate our test kits and test drivers to Locators to enjoy the benefits given to us by this new API: [actionability check](https://playwright.dev/docs/actionability), more strictness (detailed below), and in our React application - reduced flakiness. 

From our experience, we can say that simply replacing ElementHandles with Locators in a test can resolve flakiness by itself.

### What's wrong with ElementHandles?
Each `ElementHandle` refers to an actual specific DOM node. React, when trying to reconcile changes, might replace these referred DOM nodes. This is happening due to changes or as a result of components being unmounted and remounted again, making the referenced `ElementHandle` irrelevant. Keeping references to specific DOM nodes is not really needed because we usually get those references with selectors — which are agnostic to specific DOM nodes.

### How Locators help us to get the correct DOM node

- Locators keep the selector itself rather than a reference to a specific DOM node.
- Upon action (e.g .click()) the locator:
  - Uses the selector to query the DOM node relevant for that try
  - Verifies it is actionable (attached, clickable, etc.) 
  - Validates there is no single-multiple mismatch.
  - Repeats the process until it succeeds.

The actionability validation is batched along with the action itself as an atomic action. 
For example, an atomic action could be: check if the button is available, visible, clickable and only then click it — meaning less communication between node and the browser.

By doing the query and validation alongside the action, we prevent possible race conditions that could occur between waitFor -> client re-render -> action.

### Some more benefits

- Increased strictness: default locator will throw an exception if the selector matched more than one element.
- More readable errors: depicts the exact issue of why an action cannot be done instead of a failing assertion or some generic timeout.

## Final words

Battling flakiness isn’t a short-term thing. 

It requires developers' awareness and care, writing tests more carefully, keeping in mind possible race conditions, and a conscience that tells them it isn’t okay to just rerun tests.

It requires assistive tooling for testing the test itself and monitoring it.

It requires priority, time, and guidelines — things you should receive from the technical management, thus requiring them to be aware of this issue.

A single developer cannot change the state of flakiness — a group effort is needed.

Flakiness is a manageable long-term battle. Empower yourself with the right tools to not only increase your development velocity, but also elevate your overall experience.

## Sources:

- [Shopify Engineering: The Unreasonable Effectiveness of Test Retries: An Android Monorepo Case Study](https://shopify.engineering/unreasonable-effectiveness-test-retries-android-monorepo-case-study)
- [Spotify Engineering: Test Flakiness – Methods for identifying and dealing with flaky tests](https://engineering.atspotify.com/2019/11/test-flakiness-methods-for-identifying-and-dealing-with-flaky-tests/)
- [Google Testing Blog: Flaky Tests at Google and How We Mitigate Them](https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html)
- [Rethinking Productivity in Software Engineering, Edited by Caitlin Sadowski, Thomas Zimmermann - page 139 - The Cost of Context Switching](https://library.oapen.org/viewer/web/viewer.html?file=/bitstream/handle/20.500.12657/22839/1007322.pdf?sequence=1&isAllowed=y)
- [Playwright: newCDPSession](https://playwright.dev/docs/api/class-browsercontext#browser-context-new-cdp-session)