7 Key Playwright Techniques to Eliminate Test Flakiness and Boost Reliability
Test automation speeds up the testing and development life cycle, but we often notice that sometimes automated UI tests fail unpredictably without an obvious bug in the application.
We call this "Test Flakiness."
The main factors that contribute to test flakiness are:
- Timing issues: Asynchronous operations and race conditions
- Environment changes: Differences in test environments and/or devices
- External dependencies: Unreliable third-party services or APIs
- State management: Inconsistent application states between test cases.
While these factors can make test automation challenging, modern testing frameworks like Playwright offer effective solutions to combat flakiness.
In this article, we'll discuss seven key playwright techniques to reduce test flakiness and maintain the reliability of automated test cases. I'll draw on my experience working with an e-commerce project, giving you practical insight into its application in a real-world scenario.
Note that to understand this article, you must have some experience with Playwright and TypeScript.
With that out of the way, let’s start with the first technique.
1. Automatic Waiting
Automatic Waiting in Playwright lets you wait for elements to be ready before interacting with them, helping you avoid unnecessary sleep calls and reducing timing issues.
You can use page.waitForSelector()
and elementHandle.waitFor()
methods for automatic waiting.
await page.waitForSelector('#login-button', {state: 'visible'});
await page.click('#login-button');
In this example, Playwright will automatically wait until the login-button
is visible on the page. Only then will it perform the click event. This way, you don't need to worry about how long it takes for the login button to appear under different network conditions.
This approach was particularly helpful when I developed an end-to-end test suite for an e-commerce project with dynamic web elements such as item availability, total review count, and product ratings.
2. Context Isolation
Playwright supports multiple browser contexts within one instance. Using different browser contexts helps maintain a consistent state across tests while preventing contamination from previous test runs. You can use browser.newContext()
to create fresh contexts for each test as in the example below.
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('<https://example.com>');
This approach isolates test cases to ensure that one test case's state doesn't influence others. In my e-commerce project, I used a new context in every test to avoid mixing up the state of the previous test, as the testing process required checking out several products after adding them to the cart. Different browser contexts helped maintain fresh cookies, local storage, and session data.
3. Managing Dynamic Content
In modern web applications, dynamic content often changes. Playwright's locator
API offers more robust ways to interact with such dynamic elements compared to traditional locator strategies. Here’s an example:
const submitButton = page.locator('button', { hasText: 'Submit' });
await submitButton.waitFor();
Using this approach, you don't need to worry about the exact text of the submit button. This will work even if the button text is "Submit Form" because hasText
checks for any matches in buttons containing the text "Submit". This approach was key in reducing flakiness in my e-commerce project as e-commerce sites typically contain more dynamic elements than static content.
4. Custom Retries and Timeout Management
Even with automatic waiting, intermittent issues such as network failures may still cause test flakiness. A custom retry implementation can mitigate these issues.
For instance,
const retryOpertaion = async(operation, retries = 3) => { // Maximum retry attempts = 3
for(let i = 0; i < retries; i++) {
try {
return await operation();
} catch(e) {
if(i === retries - 1) throw e;
}
}
};
// Usage
await retryOpertaion(async() => {
await page.click(#submit-button);
});
This approach retries an operation for a given number of attempts before failing, which can help overcome intermittent issues. As an example, my e-commerce application's product listing page occasionally takes longer to load, causing failures. This implementation solved such sporadic issues stemming from network failures or page loading delays inside our CI/CD pipeline.
5. State Cleanup
A major cause of flaky tests is leftover state from previous tests. Cleaning up state after each test can resolve this issue. You can use Playwright's afterEach
hook to clean up the state between tests.
For an example,
afterEach(async() => {
await page.evaluate(() => {
localStorage.clear();
});
});
The code above clears the browser's local storage, ensuring each test starts with a clean slate. I also used this method for my e-commerce application. Part of my test process involved using multiple user accounts to log in and interact with the app. By clearing local storage, I was able to remove previous users' cookies, allowing for more reliable tests.
6. Robust Error Handling and Debugging
Efficient error handling and debugging strategies can identify and fix flaky tests. Using debug logs, adding screenshots of the steps, and including screen recordings of the test in the Playwright report are some of the efficient debugging strategies you can use to identify the root causes of the flaky tests.
The code snippet below adds a screenshot and screen recording to the Playwright report.
await page.screenshot({ path: 'after-login-screenshot.png' });
await page.video().saveAs('login-test.mp4');
This is my favorite debugging approach because when your test suite is integrated with the CI/CD pipeline, it’s the easiest way to troubleshoot the root cause. The screen recording will help you to understand the exact point of flakiness.
7. Network Interception
Unreliable network requests can affect test results. Playwright lets you intercept and simulate network requests, which can boost the reliability of tests that use external APIs.
You can use page.route()
to mock network responses as in the example below.
await page.route('**/api/data', (route)=> {
route.fulfill({
contentType: 'application/json',
body: JSON.stringfy({ key: 'value' })
});
});
You can apply this method, particularly when your project involves third-party APIs, allowing you to replace actual backend requests with mock data to test your application's functionality
Best practices for test design
Even when using all seven techniques in Playwright, following best practices remains important. Below are some best practices you can follow to maintain robust tests.
- Avoid fixed delays: Fixed delays can lead to unreliable tests. As a solution, use Playwright's waiting mechanisms such as
waitFor()
- Keep tests independent: Make sure tests don't rely on other tests.
- Review and update tests regularly: This helps keep your test suite in sync with changes in the application.
References
MagicPod is a no-code AI-driven test automation platform for testing mobile and web applications designed to speed up release cycles. Unlike traditional "record & playback" tools, MagicPod uses an AI self-healing mechanism. This means your test scripts are automatically updated when the application's UI changes, significantly reducing maintenance overhead and helping teams focus on development.