Skip to main content
100%

Flaky test tooling

✓ Published0🌍 Public
HHerbCaudill
Last edited Dec 21, 2023
Created on Dec 21, 2023

This example demonstrates a custom tool for debugging flaky end-to-end tests by automating the tedious process of comparing good and bad test runs. The script repeatedly runs a Playwright test until it captures both a passing and failing log, then cleans both outputs by stripping ANSI codes, timestamps, and hashes while replacing identifiers with readable tokens. Finally, it commits both cleaned logs to git so they can be diffed side by side, making visual pattern-matching in the logs much easier. Built with React and Node.js, this tooling exemplifies a practical, programmatic approach to diagnosing intermittent failures in concurrent multi-browser test environments.

AI-generated description

Debugging an intermittent test failure

I just spent 10+ days on a single bug. Posting these notes in case they're helpful to someone else (or to future me!) in a similar situation.

The bug manifested as an intermittent test failure. This was an end-to-end test using Playwright, and it involved multiple automated browsers connecting to a server and doing things concurrently.

Each browser produced hundreds of lines of debug logging, and so did the server; and for most of the time I was trying to solve this problem, I was:

  • Running the test multiple times until I got both a failure and a success
  • Copying the logs from the browser and from the server into text files
  • Running a VS Code macro that did a bunch of search-and-replace work to reduce the visual noise in the logs
  • Diffing the good and bad logs in VS Code to see what was happening differently
  • Tweaking something in the code (adding logging somewhere, adding pauses, changing the order in which things were done, etc.)
  • Repeating all of the above

This was extremely laborious and error-prone: I had lots of opportunities to miss a step, or get distracted, or lose my train of thought altogether.

The only way I managed to get out of this soul-destroying quagmire was to take a bit of time to create some tooling to automate this process so that I could get a fast feedback loop going.

First I needed to get the logging from both the browser and the server into a single stream.

I did this in a Playwright using a base page class that looks like this:

export class BasePage {
  userName?: string
  teamName?: string

  constructor(readonly page: Page) {
    this.page = page
  }

  // GENERAL

  log(msg: ConsoleMessage) {
    const text: string = msg.text().replace(/(color: #([0-9A-F]{6}))|(color: inherit)|%c/g, '')
    console.log(text)
  }

  async start() {
    await this.page.goto('/')

    // feed browser logs to test output
    this.page.on('console', msg => {
      this.log(msg)
    })

    // enable debug logging
    await this.page.evaluate(`window.localStorage.setItem('debug', '*')`)
    // reload so these take effect
    await pause(500)
    await this.page.reload()
    return this
  }

  // ...
}

I'm using the debug-js library for debug logging, and it took me a while to figure out how to get the debug variable set correctly: You have to add it to local storage, give that a moment to settle, and then reload the page.

Once that's done, you can just listen for console events from the browser and re-log them in the test code that's running in the terminal.

Then I wrote the script shown in flaky.js, which automates the steps listed above. It runs the test until it gets one success and one failure, each time piping the output to a text file.

It then runs each set of logs through a process that is intended to make it possible to diff the logs with a minimum of visual noise.

In my case there were lots of hashes and guids that were different on each run; these needed to be replaced with tokens that would be the same across runs.

I also replaced certain key bits of text with emoji, which made the logs easier to scan.

Once I had this set up, I was able to pin down the problem within a few hours.

Here's a screenshot of that diff; in the terminal you can see the test run where it finally stopped failing. 🥳

screenshot

Similar vizzes