<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mykhailo Krasnovskyi</title>
    <description>The latest articles on DEV Community by Mykhailo Krasnovskyi (@krasmik).</description>
    <link>https://dev.to/krasmik</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4063428%2Fa07ac080-fd38-40f6-a6e0-7415b7b83fd0.jpg</url>
      <title>DEV Community: Mykhailo Krasnovskyi</title>
      <link>https://dev.to/krasmik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/krasmik"/>
    <language>en</language>
    <item>
      <title>When Should You Refactor Automated Tests? 7 Warning Signs</title>
      <dc:creator>Mykhailo Krasnovskyi</dc:creator>
      <pubDate>Mon, 12 Oct 2026 11:24:28 +0000</pubDate>
      <link>https://dev.to/krasmik/when-should-you-refactor-automated-tests-7-warning-signs-46pa</link>
      <guid>https://dev.to/krasmik/when-should-you-refactor-automated-tests-7-warning-signs-46pa</guid>
      <description>&lt;p&gt;Only refactor your automated tests when they are still testing the correct things, but the code underlying them makes each change slow, fragile, or risky, and only rewrite them when they are testing the wrong things or when the framework can no longer keep up with where your product is heading.&lt;/p&gt;

&lt;p&gt;The majority of suites should be refactored, while only a small number need to be rewritten. &lt;strong&gt;The expensive error is to confuse the two.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 7 Warning Signs
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;One product change breaks dozens of tests&lt;/li&gt;
&lt;li&gt;Every test starts with 40 lines of setup&lt;/li&gt;
&lt;li&gt;Sleeps and retries hold the suite together&lt;/li&gt;
&lt;li&gt;Tests only pass in one order&lt;/li&gt;
&lt;li&gt;Nobody can tell what a test checks&lt;/li&gt;
&lt;li&gt;UI tests check logic that doesn't need a UI&lt;/li&gt;
&lt;li&gt;Only one person can fix a broken test&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Entries 1 through 5 and 7 generally require a refactor; a rewrite is justified only when the framework, technology stack, or testing strategy is no longer suitable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;A designer changes the name of one button, renaming "Checkout" to "Continue to payment," and the following morning, 63 automated tests fail.&lt;/p&gt;

&lt;p&gt;The product functions properly, but the pipeline shows up as red, and one locator, which has been copied into 63 files, occupies the entire day of two automation engineers.&lt;/p&gt;

&lt;p&gt;The true issue isn't whether we should fix these tests; it's the cost this suite's structure imposes on you each time the product changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Test Code Rots Faster Than Product Code
&lt;/h2&gt;

&lt;p&gt;The product code receives a code review, has architecture discussions, and is supervised by a tech lead who cares about structure; test code, on the other hand, usually gets none of these things, is written under a deadline, is merged as soon as it passes, and is then ignored until it stops working.&lt;/p&gt;

&lt;p&gt;3 habits speed up the decay:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The act of copying and pasting is safe. It's better than creating a helper when you have 200 copies of the same setup.&lt;/li&gt;
&lt;li&gt;There is no monitoring of test maintenance, and the time spent on fixing tests is absorbed into "QA work."&lt;/li&gt;
&lt;li&gt;The tests persist after the people who created them have left, and subsequent engineers continue using the framework even if they do not fully understand it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. One Product Change Breaks Dozens of Tests
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The issue is that a single item, for example, a locator, URL, or label, is stored in numerous locations.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;javascript&lt;br&gt;
// test_checkout_guest.js&lt;br&gt;
await page.click('#btn-checkout-v2');&lt;/p&gt;

&lt;p&gt;// test_checkout_promo.js&lt;br&gt;
await page.click('#btn-checkout-v2');&lt;/p&gt;

&lt;p&gt;// ...and 61 more files&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix: **use the **Page Object pattern.&lt;/strong&gt; Each screen gets a class that holds its locators and actions. Tests call those actions instead of touching elements directly.&lt;/p&gt;

&lt;p&gt;javascript&lt;br&gt;
// pages/CartPage.js&lt;br&gt;
export class CartPage {&lt;br&gt;
  constructor(page) { this.page = page; }&lt;br&gt;
  async proceedToCheckout() {&lt;br&gt;
    await this.page.getByTestId('checkout-button').click();&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result is a single product change and a single test change.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Every Test Starts With 40 Lines of Setup
&lt;/h2&gt;

&lt;p&gt;You have to scroll past half a screen worth of login screens, data entry, and navigation before getting to the actual check, by which time the setup process has taken control of the suite.&lt;/p&gt;

&lt;p&gt;It hurts in 3 ways:&lt;/p&gt;

&lt;p&gt;-&lt;strong&gt;Slow runs.&lt;/strong&gt;Every test logs in through the UI, even when it only checks a report page.&lt;br&gt;
-&lt;strong&gt;Buried intent.&lt;/strong&gt;The real assertion drowns in boilerplate, and reviewers stop reading.&lt;br&gt;
-&lt;strong&gt;Painful updates.&lt;/strong&gt;Change the login flow, and every copied setup breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The solution:&lt;/strong&gt; is to move the setup process into the fixtures and helpers. Log in once via the API and then reuse the session. Use factory functions to create the test data rather than filling in the forms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; is that the tests are shorter, the runs take less time, and new engineers are able to read the suite on their first day.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Sleeps and Retries Hold the Suite Together
&lt;/h2&gt;

&lt;p&gt;Search your code for sleep, wait(5000), Thread.sleep, and retry annotations. &lt;strong&gt;Each one marks a failure someone patched instead of fixing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The patches cost you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slower runs&lt;/strong&gt; with every hard-coded pause&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flakiness that is hidden - the flaky tests&lt;/strong&gt; (that is, tests which pass and fail when the same code is run without any changes) - remain flaky even after retries.&lt;/li&gt;
&lt;li&gt;Bugs in the products are masked, while the real timing problems are accompanied by testing issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The solution is to substitute the use of fixed sleeps with waits for an actual condition - such as an element appearing, a request finishing, or an API returning the expected status - and to eliminate each retry in turn while addressing the underlying cause.&lt;/p&gt;

&lt;p&gt;The result is that feedback becomes faster, and the occurrence of red builds up trust within the team.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Tests Only Pass in One Order
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Run the suite in a random order.&lt;/strong&gt; If it then fails, it is because the tests depend on one another.&lt;/p&gt;

&lt;p&gt;Common symptoms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test B expects a user that Test A created&lt;/li&gt;
&lt;li&gt;Tests share one account, and one test changes its settings for all&lt;/li&gt;
&lt;li&gt;The suite passes locally but fails in parallel CI runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The importance lies in the fact that order-dependent tests prevent parallel execution and assign fault to the wrong test; test B fails even though it was caused by test A.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; make every test independent. Each one &lt;strong&gt;creates its own data, cleans up after itself,&lt;/strong&gt; and assumes nothing about what ran before.&lt;/p&gt;

&lt;p&gt;The result is parallel runs, shorter pipelines, and failures that indicate the correct area.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Nobody Can Tell What a Test Checks
&lt;/h2&gt;

&lt;p&gt;If there's a test your team hasn't written recently and you can't explain in one sentence what bug it would detect, then the test has lost its value, even if it passes.&lt;/p&gt;

&lt;p&gt;Warning signs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vague names&lt;/strong&gt; like test_checkout_3 or testFlowNew&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No assertions.&lt;/strong&gt; The test clicks through a flow and passes as long as nothing crashes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Giant assertions.&lt;/strong&gt; Thirty fields checked at once, so nobody knows which one mattered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commented-out checks&lt;/strong&gt; disabled "temporarily"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; name tests after the behavior they protect, for example guest_user_can_apply_promo_code_at_checkout. &lt;strong&gt;Verify one clear outcome per test and delete tests that can't justify themselves.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The payoff:&lt;/strong&gt; a failing test's name tells your team exactly what broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. UI Tests Check Logic That Doesn't Need a UI
&lt;/h2&gt;

&lt;p&gt;The tax calculations, the discount rules, the permission checks, and the data validation constitute business logic and can be checked at the API level in a matter of milliseconds without being affected by any layout changes.&lt;/p&gt;

&lt;p&gt;When most checks run through the UI, you pay the highest price for each one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Longest execution time&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most flakines&lt;/strong&gt;s from rendering and timing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most maintenance&lt;/strong&gt; whenever the design changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; transfer the logic checks to the API or service layer and continue to carry out UI tests for those situations in which only a UI can provide proof, such as users being able to complete key processes like signing up and checking out.&lt;/p&gt;

&lt;p&gt;The result is a set of tools that are both faster and cheaper while still protecting critical flows.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Only One Person Can Fix a Broken Test
&lt;/h2&gt;

&lt;p&gt;When the same engineer is the one people refer to in the case of a failed payment test, the issue is structural rather than staffing-related.&lt;/p&gt;

&lt;p&gt;It looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New automation engineers need weeks before touching the framework&lt;/li&gt;
&lt;li&gt;Custom utilities have no documentation&lt;/li&gt;
&lt;li&gt;Nobody remembers why key framework decisions were made&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; when that person is out or leaves, &lt;strong&gt;the suite stalls.&lt;/strong&gt; Broken tests pile up, and quarantined tests (tests pulled from runs "for now") become permanent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; simplify custom layers, &lt;strong&gt;follow patterns your team already knows,&lt;/strong&gt; and keep a short README next to the code explaining how the framework fits together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The payoff:&lt;/strong&gt; any engineer can maintain the suite, and it survives team changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture: Test Automation Debt
&lt;/h2&gt;

&lt;p&gt;Such signs seldom occur in isolation; taken together, they constitute what is known as test automation debt, that is, the increasing cost of maintaining a suite relative to the value it provides.&lt;/p&gt;

&lt;p&gt;Refactoring clears some of that debt. Deciding which fixes to make first and proving they have worked requires a more comprehensive plan. The guide on test automation debt explains how to evaluate the debt and prioritize the work to clean it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refactor vs. Rewrite: How to Decide
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;An unnecessary rewrite takes months. The time required to refactor a suite that needed a rewrite is used up at a slower rate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7izlkkz9sgpagazmm47g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7izlkkz9sgpagazmm47g.png" alt=" " width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clear signals you need a rewrite&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You can't upgrade the framework&lt;/strong&gt; without breaking most of the suite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tool doesn't fit your stack,&lt;/strong&gt; such as a desktop-era tool testing a modern single-page or mobile app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The suite never matched your real risks.&lt;/strong&gt; Low-value screens are heavily covered, while payments and data integrity get little.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A refactor would touch more than half the tests&lt;/strong&gt; anyway&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Still unsure? Run a POC&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Build one module within the new structure as a Proof of Concept and run it alongside the existing one for a few sprints before comparing the execution time, flakiness, and maintenance hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real numbers settle the debate faster than meetings.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mistake Teams Keep Making
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The most common mistake is to halt all testing activity to "fix the suite" in a single major effort.&lt;/strong&gt; This approach almost never succeeds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feature work continues, so the product changes under the refactor&lt;/li&gt;
&lt;li&gt;No new tests get written for weeks, leaving gaps in fresh code&lt;/li&gt;
&lt;li&gt;The project overruns and gets cut halfway&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The other mistake is not arranging the work and then hoping that the engineers will deal with it "when they have time" - deadlines always prevail.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to find out more about what should be fixed, rewritten, or deleted, see our &lt;a href="https://www.qamadness.com/test-automation-debt-when-your-test-suite-stops-delivering/" rel="noopener noreferrer"&gt;test automation debt guide&lt;/a&gt;. That document explains how to prioritize the cleanup without stopping new work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated Test Refactoring Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Refactor in slices.&lt;/strong&gt; Fix one pattern, like locators or setup, across one module, then move on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the suite green.&lt;/strong&gt; Refactoring changes structure, not behavior. If a result changes, stop and investigate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate refactoring from new tests.&lt;/strong&gt; Mixed pull requests hide mistakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touch it, clean it.&lt;/strong&gt; Whoever edits a test leaves it slightly better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review test code like product code.&lt;/strong&gt; Same standards, same reviewers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure before and after.&lt;/strong&gt; Track maintenance hours per sprint, execution time and flakiness to show what the work delivered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reserve capacity.&lt;/strong&gt; A fixed share of every sprint beats a quarterly cleanup that never happens.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick Gut Check
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One product change breaks many tests?&lt;/strong&gt; Refactor locators and page objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineers rerun red builds without reading them?&lt;/strong&gt; Fix waits, retries and test independence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A new engineer can't understand a test in two minutes?&lt;/strong&gt; Improve names, setup and assertions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI tests check business logic?&lt;/strong&gt; Move those checks to the API layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework upgrades cause chaos?&lt;/strong&gt; Consider a rewrite, starting with a POC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests don't guard the flows that cost money?&lt;/strong&gt; Rethinking your test strategy matters more than cleaner code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to Get Help
&lt;/h2&gt;

&lt;p&gt;Most in-house teams know their suite needs work. &lt;strong&gt;They don't have the hours.&lt;/strong&gt; Engineers keep today's pipeline alive, developers ship features, and the refactor keeps sliding.&lt;/p&gt;

&lt;p&gt;Our &lt;a href="https://www.qamadness.com/services/automated-testing/" rel="noopener noreferrer"&gt;automated testing services&lt;/a&gt; team is &lt;strong&gt;100% Middle and Senior QA Engineers.&lt;/strong&gt; They assess your suite, decide with you whether it needs a refactor or a rewrite, and do the work alongside your releases, &lt;strong&gt;with no constant supervision from your side.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The result: &lt;strong&gt;faster runs, fewer false alarms and a suite that keeps pace with your product.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Want a straight answer on what your suite needs? &lt;strong&gt;&lt;a href="https://www.qamadness.com/contact-us/" rel="noopener noreferrer"&gt;Request a QA Assessment,&lt;/a&gt;&lt;/strong&gt; and we'll show you where to start.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>automatedtest</category>
      <category>testautomation</category>
      <category>qa</category>
    </item>
    <item>
      <title>API Contract Testing vs Integration Testing: What's the Difference?</title>
      <dc:creator>Mykhailo Krasnovskyi</dc:creator>
      <pubDate>Fri, 25 Sep 2026 15:02:31 +0000</pubDate>
      <link>https://dev.to/krasmik/api-contract-testing-vs-integration-testing-whats-the-difference-22dj</link>
      <guid>https://dev.to/krasmik/api-contract-testing-vs-integration-testing-whats-the-difference-22dj</guid>
      <description>&lt;p&gt;A team we worked with ran 400 integration tests before every release. Full green suite. Deploy on Friday.&lt;/p&gt;

&lt;p&gt;Payments broke in production two hours later.&lt;/p&gt;

&lt;p&gt;The cause? A field got renamed in the response body. Every integration test ran on a shared staging environment where both services happened to be on compatible versions that week. The contract had already drifted underneath them.&lt;/p&gt;

&lt;p&gt;Nothing local caught it. Nothing local was actually checking the contract. It was checking whether two specific deployed versions got along that particular day.&lt;/p&gt;

&lt;p&gt;This mix-up costs teams real money. It comes from treating contract testing and integration testing like the same thing, wearing two different names. They're not. &lt;strong&gt;They catch different bugs, run at different speeds, and belong in different parts of your pipeline.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The One-Sentence Version
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Integration testing&lt;/strong&gt; checks whether real, running services work together correctly, end-to-end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contract testing&lt;/strong&gt; verifies that two services agree on the shape of their communication without either running the other's actual code.&lt;/p&gt;

&lt;p&gt;That's the whole split. Everything else is detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Integration Testing Actually Tests
&lt;/h2&gt;

&lt;p&gt;Integration tests spin up real dependencies. Real database. Real message queue. Real downstream API, or at least a faithful stand-in for one.&lt;/p&gt;

&lt;p&gt;Then they run an actual scenario through the stack and check what comes out the other end.&lt;/p&gt;

&lt;p&gt;A typical integration test for an order flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create an order through the API&lt;/li&gt;
&lt;li&gt;Confirm it lands in the database with the right status&lt;/li&gt;
&lt;li&gt;Confirm the payment service gets called and returns a charge ID&lt;/li&gt;
&lt;li&gt;Confirm the notification service sends a confirmation email&lt;/li&gt;
&lt;li&gt;Confirm the order status updates to "confirmed"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is valuable work. It's the only way to catch bugs living in the interaction itself, not just the interface.&lt;/p&gt;

&lt;p&gt;Race conditions. Transaction rollbacks. Timing issues. Real business logic tangled across services. If your discount logic breaks when a coupon code and a loyalty tier stack in a specific order, only a real integration test finds that. A contract doesn't know what a coupon is.&lt;/p&gt;

&lt;p&gt;But integration tests carry a cost that gets uglier as your architecture grows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;They need everything running at once.&lt;/strong&gt; Every dependent service, healthy, in sync.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They're slow.&lt;/strong&gt; Minutes, not seconds, once you're past a trivial suite.
-** They're environment-fragile.** A test passes locally, then fails in CI because staging runs a different service version.
-** Failures stay vague.** Something broke. Rarely what, or where, without real digging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At three services, none of these bite hard. At fifteen, it starts eating whole afternoons.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh1098x6qrbc9yl45okuj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh1098x6qrbc9yl45okuj.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Contract Testing Actually Tests
&lt;/h2&gt;

&lt;p&gt;Contract testing skips the "does this whole thing work" question. It asks a narrower one instead: &lt;strong&gt;&lt;em&gt;Does this API still look the way its consumers expect it to look?&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No live dependency chain. No shared staging environment.&lt;/p&gt;

&lt;p&gt;The consumer service — say, checkout — writes a test against a mock of the provider, in this case, payments. It states exactly what request it sends and what response shape it expects in return. That expectation gets saved as a contract, usually a JSON file.&lt;/p&gt;

&lt;p&gt;The provider team then runs that contract against their real, running service, in isolation, with zero consumer code involved.&lt;/p&gt;

&lt;p&gt;If the provider's real response doesn't match the contract, verification fails. Right there. In the provider's own build. Before anything ships anywhere.&lt;/p&gt;

&lt;p&gt;Here's what a Pact consumer test looks like in practice:&lt;/p&gt;

&lt;p&gt;provider&lt;br&gt;
  .given('account 123 exists')&lt;br&gt;
  .uponReceiving('a request for account details')&lt;br&gt;
  .withRequest({&lt;br&gt;
    method: 'GET',&lt;br&gt;
    path: '/accounts/123',&lt;br&gt;
  })&lt;br&gt;
  .willRespondWith({&lt;br&gt;
    status: 200,&lt;br&gt;
    body: {&lt;br&gt;
      id: '123',&lt;br&gt;
      balance: like(500.00),&lt;br&gt;
    },&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;Notice what's missing. No real payment service. No database. No network call to anything live. Just a statement of expectations, turned into a check that actually runs.&lt;/p&gt;

&lt;p&gt;Contract tests are fast — seconds, because there's nothing to spin up. They're precise about failure, because a broken contract points straight at the field or endpoint that changed.&lt;/p&gt;

&lt;p&gt;And they scale in a way integration tests never do. Add a sixteenth service, and you get one more contract. Not a combinatorial mess of environments to keep in sync.&lt;/p&gt;

&lt;p&gt;What they can't do: tell you the business logic is correct. A contract test confirms that balance is a number. It has zero opinion on whether that number is the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-Side Comparison
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Integration Testing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What runs: Real services, real dependencies&lt;/li&gt;
&lt;li&gt;What it catches: Business logic bugs, timing issues, real end-to-end failures&lt;/li&gt;
&lt;li&gt;Speed: Slow — minutes per run&lt;/li&gt;
&lt;li&gt;Environment needs: Every dependent service running and healthy&lt;/li&gt;
&lt;li&gt;Failure clarity: Vague — something broke somewhere in the chain&lt;/li&gt;
&lt;li&gt;Scales with service count: Badly — more services means more coordination&lt;/li&gt;
&lt;li&gt;Coverage of user journeys: Yes, full flows&lt;/li&gt;
&lt;li&gt;Coverage of API shape drift: Only if the test happens to touch the changed field&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Contract Testing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What runs: Mocked provider on the consumer side, real provider in isolation on the verification side&lt;/li&gt;
&lt;li&gt;What it catches: Shape mismatches, breaking API changes, drift between what's expected and what's shipped&lt;/li&gt;
&lt;li&gt;Speed: Fast — seconds&lt;/li&gt;
&lt;li&gt;Environment needs: None — no shared environment at all&lt;/li&gt;
&lt;li&gt;Failure clarity: Precise — this field, this endpoint, this contract&lt;/li&gt;
&lt;li&gt;Scales with service count: Well — more services means more contracts, not more coordination&lt;/li&gt;
&lt;li&gt;Coverage of user journeys: No — single interactions only&lt;/li&gt;
&lt;li&gt;Coverage of API shape drift: Yes, by design&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where Contract Testing Wins Outright
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Independent deployments.&lt;/strong&gt; Payments wants to ship three times a day. Checkout ships weekly. Integration tests force coordination, or they run against stale versions and lie to you. Contract tests let payments verify against every active consumer contract on every build. Checkout verifies its own expectations without waiting for payments to deploy anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fast feedback in CI.&lt;/strong&gt; A contract test suite for one service boundary runs in seconds. You get an answer before your coffee's done. An integration suite covering the same boundary, with real dependencies, easily runs ten or twenty times longer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment gating.&lt;/strong&gt; Most teams miss this part entirely. Tools like Pact support a can-i-deploy check. Before a service ships, it asks a broker whether its current version has been verified against every contract still in force. No means the deploy stops. Integration testing doesn't give you anything like this — it doesn't produce a portable, queryable compatibility record. Just a pass or fail for one run, in one environment, at one moment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging speed.&lt;/strong&gt; A contract test fails, you know exactly what changed. An integration test fails, you're digging through logs across four services trying to figure out which one lied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Integration Testing Wins Outright
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Real bugs in real interactions.&lt;/strong&gt; Contract testing checks shape. It has no idea whether your refund logic correctly triggers a retention workflow for enterprise annual accounts, but skips it for monthly ones. That's business logic playing out across services. Only a real integration test, hitting real code with a real scenario, catches that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full user journeys.&lt;/strong&gt; Checkout isn't one API call. It's a cart, inventory check, payment, notification, order confirmation — in sequence, with state carried through the whole thing. Contract tests check each boundary on its own. They say nothing about whether the full journey holds together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data consistency across writes.&lt;/strong&gt; Need to confirm a write to one service correctly triggers a downstream read in another, with real data flowing through? Contract testing can't help you there. That's an integration test's job. Full stop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catching timing and race conditions.&lt;/strong&gt; Contract tests are stateless snapshots of expected shape. They don't run concurrently against a shared state. They can't surface the kind of race condition that only shows up when two requests hit a database in the wrong order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mistake Teams Keep Making
&lt;/h2&gt;

&lt;p&gt;The common failure isn't picking the wrong tool. It's picking one tool and dropping the other entirely.&lt;/p&gt;

&lt;p&gt;Teams that go all-in on integration testing end up with slow pipelines, flaky environments, and breaking changes slipping through. The test that would've caught it wasn't run that day because it required a service that was down for maintenance.&lt;/p&gt;

&lt;p&gt;Teams that go all-in on contract testing get fast, precise pipelines that miss real bugs in business logic. Nobody's running a full order through the system anymore. Everyone's happy their contracts are green while checkout quietly breaks in a way no contract could ever catch.&lt;/p&gt;

&lt;p&gt;Neither approach alone is a real strategy. &lt;strong&gt;They're complementary layers, not competitors.&lt;/strong&gt; That's the part that gets lost whenever someone tries to crown a winner.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Actually Split the Work
&lt;/h2&gt;

&lt;p&gt;A reasonable line most teams land on, after enough pain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contract tests for every service-to-service API boundary. Every consumer states what it needs. Every provider verifies it on every build.&lt;/li&gt;
&lt;li&gt;A smaller set of integration tests for the handful of user journeys that actually matter to the business. Checkout, signup — the flows that lose you money or customers if they break.&lt;/li&gt;
&lt;li&gt;Deployment gates built on contract verification, not on waiting for a full integration suite to go green across every environment.&lt;/li&gt;
&lt;li&gt;Integration tests running less often — nightly, or before major releases — instead of on every single commit. They're expensive, and running them 40 times a day buys you almost nothing extra.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This won't fit every team exactly. A payments-heavy fintech product probably needs more integration coverage on money-moving flows than a content platform does. But the shape of the split — contracts for boundaries, integration for journeys — holds up across most architectures we've seen.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick Gut Check
&lt;/h2&gt;

&lt;p&gt;Not sure which one you need for a given test? Ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Am I checking whether two services &lt;strong&gt;agree on a shape?&lt;/strong&gt; Contract test.&lt;/li&gt;
&lt;li&gt;Am I checking whether** a real scenario produces the right outcome? **Integration test.&lt;/li&gt;
&lt;li&gt;Do I need this to run in &lt;strong&gt;seconds, on every commit?&lt;/strong&gt; Contract test.&lt;/li&gt;
&lt;li&gt;Do I need this to catch &lt;strong&gt;a bug that only shows up when real code runs against real code?&lt;/strong&gt; Integration test.&lt;/li&gt;
&lt;li&gt;Am I trying to &lt;strong&gt;stop a bad deployment before it ships?&lt;/strong&gt; Contract test, with a gate.&lt;/li&gt;
&lt;li&gt;Am I trying to &lt;strong&gt;prove the checkout flow works end to end?&lt;/strong&gt; Integration test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams don't need to pick one. They need both, sized right, each doing the job it's actually good at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Go Deeper
&lt;/h2&gt;

&lt;p&gt;Running microservices and breaking changes keep slipping through to production? The fix usually isn't more integration tests. It's contract testing at the boundaries, wired into your pipeline as a deployment gate.&lt;/p&gt;

&lt;p&gt;Our &lt;a href="https://www.qamadness.com/api-contract-testing-for-microservices/" rel="noopener noreferrer"&gt;guide on API contract testing for microservices&lt;/a&gt; walks through exactly how to set that up — including the Pact broker workflow and the can-i-deploy check that stops a broken contract before it ever reaches a real user.&lt;/p&gt;

</description>
      <category>api</category>
      <category>testing</category>
      <category>qa</category>
      <category>performance</category>
    </item>
    <item>
      <title>How to Reduce Flaky Tests in CI/CD Without Rewriting Your Whole Test Suite</title>
      <dc:creator>Mykhailo Krasnovskyi</dc:creator>
      <pubDate>Fri, 07 Aug 2026 21:26:37 +0000</pubDate>
      <link>https://dev.to/krasmik/how-to-reduce-flaky-tests-in-cicd-without-rewriting-your-whole-test-suite-5641</link>
      <guid>https://dev.to/krasmik/how-to-reduce-flaky-tests-in-cicd-without-rewriting-your-whole-test-suite-5641</guid>
      <description>&lt;p&gt;Flaky tests rarely spread evenly across a suite. In most pipelines we audit, a small group of specs produces the majority of unreliable failures. So you can usually fix the pipeline without a rewrite.&lt;/p&gt;

&lt;p&gt;This is a practical guide to stabilizing automation you already own. No definitions of flakiness, no "rewrite it in Playwright" advice. Just triage, root cause work, quarantine rules, and the metrics that tell you whether it worked.&lt;/p&gt;

&lt;p&gt;Examples cover Playwright, Cypress, Selenium, GitHub Actions, and GitLab CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can I reduce flaky tests in CI/CD without rewriting the entire test suite?
&lt;/h2&gt;

&lt;p&gt;Find the small set of specs causing most failures, classify why each one fails, fix by category instead of one test at a time, and quarantine the rest so the pipeline stays honest.&lt;/p&gt;

&lt;p&gt;A rewrite is tempting because the suite feels untrustworthy. But a rewrite moves the same design mistakes into new syntax. Hard-coded sleeps and shared test data will reappear in any framework. Fix the causes first. Then decide if you still need new tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What causes flaky tests in CI/CD pipelines?
&lt;/h2&gt;

&lt;p&gt;Almost every flaky test traces back to a small number of root causes. Classify before you fix, because the remedy differs by category.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdoyn1b3sv23t11noi0h9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdoyn1b3sv23t11noi0h9.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Selenium documentation is blunt about the first row. Race conditions between browser state and driver commands are "one of the primary causes of flaky tests." That is where most teams should start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do tests fail in CI but pass locally?
&lt;/h2&gt;

&lt;p&gt;This question comes up in every audit, and the answer is usually one of 6 things.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speed.&lt;/strong&gt; CI machines are often slower and more loaded than your laptop, so implicit timing assumptions break.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallelism.&lt;/strong&gt; Local runs are frequently single-threaded. CI runs four or eight workers that fight over the same test data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headless rendering.&lt;/strong&gt; Different viewport, no GPU, fonts missing, elements positioned differently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean state.&lt;/strong&gt; Your local browser has cookies, cached auth, and a warm database. CI starts empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locale and timezone.&lt;/strong&gt; Runners default to UTC. Date assertions written in your timezone drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network shape.&lt;/strong&gt; Local hits a dev server on localhost. CI crosses a network boundary with real latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a fast diagnostic, run your suite locally with the same worker count and a fresh browser profile. If it fails there too, the problem is your tests, not the runner.&lt;/p&gt;

&lt;h2&gt;
  
  
  How should QA teams prioritize flaky tests in an existing automation suite?
&lt;/h2&gt;

&lt;p&gt;Do not fix flaky tests in the order you find them. Rank them. Start by calculating a flake rate per spec over the last 30 days:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;flake rate = (runs that failed then passed on retry) / total runs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then score each test on three factors: flake rate, how often the spec runs, and whether it blocks merges. A test that fails 4% of the time on every pull request costs far more engineering hours than one failing 30% of the time in a nightly job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flaky test triage checklist
&lt;/h2&gt;

&lt;p&gt;Run this on each candidate before writing a fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it fail in isolation, or only in a full parallel run?&lt;/li&gt;
&lt;li&gt;Does it fail on a specific worker, browser, or shard?&lt;/li&gt;
&lt;li&gt;Is there a hard-coded sleep or fixed timeout in the failure path?&lt;/li&gt;
&lt;li&gt;Does it create or reuse data that another test touches?&lt;/li&gt;
&lt;li&gt;Does it depend on a previous test, leaving the app in a certain state?&lt;/li&gt;
&lt;li&gt;Does it call a third-party service that is not stubbed?&lt;/li&gt;
&lt;li&gt;Do the failure timestamps cluster around deploys, backups, or cron jobs?&lt;/li&gt;
&lt;li&gt;Which root cause row from the table above matches?&lt;/li&gt;
&lt;li&gt;Fix now, quarantine, or delete?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep the last question open. Some flaky tests cover logic already tested at a lower level, so deleting them is a legitimate outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you fix flaky automated tests in CI/CD?
&lt;/h2&gt;

&lt;p&gt;Fix by category. The next 3 categories cover most of the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Replace sleeps with condition-based waits
&lt;/h2&gt;

&lt;p&gt;A fixed sleep is a guess about timing. It fails when the app is slower and wastes time when it is faster.&lt;/p&gt;

&lt;p&gt;Playwright assertions retry until they pass or time out, so this is usually enough:&lt;/p&gt;

&lt;p&gt;// Fragile: assumes the row renders within 3 seconds&lt;/p&gt;

&lt;p&gt;await page.waitForTimeout(3000);&lt;/p&gt;

&lt;p&gt;await expect(page.locator('.order-row')).toHaveCount(1);&lt;/p&gt;

&lt;p&gt;// Stable: retries the condition itself&lt;/p&gt;

&lt;p&gt;await expect(page.locator('.order-row')).toHaveCount(1, { timeout: 10_000 });&lt;/p&gt;

&lt;p&gt;In Cypress, wait on the request rather than the clock:&lt;/p&gt;

&lt;p&gt;cy.intercept('POST', '/api/orders').as('createOrder');&lt;/p&gt;

&lt;p&gt;cy.get('[data-cy=submit]').click();&lt;/p&gt;

&lt;p&gt;cy.wait('@createOrder').its('response.statusCode').should('eq', 201);&lt;/p&gt;

&lt;p&gt;cy.get('[data-cy=order-row]').should('have.length', 1);&lt;/p&gt;

&lt;p&gt;For Selenium, use explicit waits and avoid one specific trap. The &lt;a href="https://www.selenium.dev/documentation/webdriver/waits/" rel="noopener noreferrer"&gt;Selenium waits&lt;/a&gt; documentation warns against mixing implicit and explicit waits, because the combination produces unpredictable timeouts:&lt;/p&gt;

&lt;p&gt;from selenium.common import NoSuchElementException, ElementNotInteractableException&lt;/p&gt;

&lt;p&gt;from selenium.webdriver.support.wait import WebDriverWait&lt;/p&gt;

&lt;p&gt;errors = [NoSuchElementException, ElementNotInteractableException]&lt;/p&gt;

&lt;p&gt;wait = WebDriverWait(driver, timeout=10, poll_frequency=0.2, ignored_exceptions=errors)&lt;/p&gt;

&lt;p&gt;wait.until(lambda d: d.find_element(By.CSS_SELECTOR, ".order-row").is_displayed())&lt;/p&gt;

&lt;p&gt;Pick one strategy per project. Explicit waits give you control per interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Give every test its own data
&lt;/h2&gt;

&lt;p&gt;Shared fixtures are the most common cause of parallel-only failures. Generate data per test and seed it through the API instead of the UI:&lt;/p&gt;

&lt;p&gt;test('user can cancel an order', async ({ page, request }) =&amp;gt; {&lt;/p&gt;

&lt;p&gt;const email = &lt;code&gt;qa+${crypto.randomUUID()}@example.com&lt;/code&gt;;&lt;/p&gt;

&lt;p&gt;const { id } = await (await request.post('/api/test/orders', {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data: { email, status: 'pending' },
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;})).json();&lt;/p&gt;

&lt;p&gt;await page.goto(&lt;code&gt;/orders/${id}&lt;/code&gt;);&lt;/p&gt;

&lt;p&gt;await page.getByRole('button', { name: 'Cancel order' }).click();&lt;/p&gt;

&lt;p&gt;await expect(page.getByText('Order cancelled')).toBeVisible();&lt;/p&gt;

&lt;p&gt;});&lt;/p&gt;

&lt;p&gt;API setup removes a long UI path from the test and cuts the number of steps that can fail for reasons unrelated to what you are testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Control the network
&lt;/h2&gt;

&lt;p&gt;Stub third-party calls in functional tests. Keep a separate, small set of contract tests that hit the real service on a schedule, so an outage at a payment provider does not block merges.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can I stabilize Playwright, Cypress, or Selenium tests?
&lt;/h2&gt;

&lt;p&gt;Framework-specific moves that pay off quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Playwright.&lt;/strong&gt; Use role and label locators over CSS chains. Enable trace: 'on-first-retry' so you get a full timeline for the failure without storing traces for every run. Keep tests isolated rather than reaching for serial mode, which the docs recommend for the same reason.&lt;/p&gt;

&lt;p&gt;// playwright.config.ts&lt;/p&gt;

&lt;p&gt;export default defineConfig({&lt;/p&gt;

&lt;p&gt;retries: process.env.CI ? 2 : 0,&lt;/p&gt;

&lt;p&gt;use: { trace: 'on-first-retry', video: 'retain-on-failure' },&lt;/p&gt;

&lt;p&gt;});&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cypress.&lt;/strong&gt; Turn on test isolation, use cy.session() for cached login, and replace cy.wait(ms) with aliased intercepts. Set retries only in run mode:&lt;/p&gt;

&lt;p&gt;// cypress.config.js&lt;/p&gt;

&lt;p&gt;retries: { runMode: 2, openMode: 0 }&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selenium.&lt;/strong&gt; Centralize waits in one helper so timeouts are consistent, pin browser and driver versions in CI, and run in a container so local and pipeline environments match.&lt;/p&gt;

&lt;p&gt;Across all three, disable CSS animations in your test build. It removes an entire class of timing failures in a single change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should engineering teams use retries for flaky tests?
&lt;/h2&gt;

&lt;p&gt;Retries are a detection tool, not a cure. Used well, they keep the pipeline moving while you fix causes. Used badly, they hide real bugs.&lt;/p&gt;

&lt;p&gt;The rule we apply: retries are allowed, but a passing-on-retry result must be recorded as flaky, not green. Playwright does this by default, sorting results into passed, flaky, and failed. Its &lt;a href="https://playwright.dev/docs/test-retries" rel="noopener noreferrer"&gt;Playwright test retries&lt;/a&gt; docs also expose testInfo.retry, which is useful for clearing server state before a second attempt:&lt;/p&gt;

&lt;p&gt;test('checkout completes', async ({ page }, testInfo) =&amp;gt; {&lt;/p&gt;

&lt;p&gt;if (testInfo.retry) await resetCartState();&lt;/p&gt;

&lt;p&gt;// ...&lt;/p&gt;

&lt;p&gt;});&lt;/p&gt;

&lt;p&gt;Two limits to set. Cap retries at two, since a test needing three attempts is broken rather than flaky. And never retry the whole pipeline job to get a green build, because that erases the signal you need.&lt;/p&gt;

&lt;p&gt;In GitLab CI, scope job-level retries to infrastructure problems only:&lt;/p&gt;

&lt;p&gt;e2e:&lt;/p&gt;

&lt;p&gt;script: npx playwright test&lt;/p&gt;

&lt;p&gt;retry:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max: 2

when:

  - runner_system_failure

  - stuck_or_timeout_failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;artifacts:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;when: always

paths: [playwright-report/]

reports:

  junit: results.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That way runner crashes get retried and genuine test failures do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should flaky tests be quarantined?
&lt;/h2&gt;

&lt;p&gt;Quarantine when a test is unreliable enough to erode trust but too valuable to delete, and you cannot fix it this sprint. It keeps the main pipeline meaningful while the test stays visible.&lt;/p&gt;

&lt;p&gt;Quarantine works only with an expiry date. Without one, the quarantine list becomes a graveyard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quarantine policy example
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Eligibility:&lt;/strong&gt; flake rate above 2% over 30 days, or 3+ false failures in a week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Action:&lt;/strong&gt; tag the test @quarantine and move it out of the blocking job.&lt;/p&gt;

&lt;p&gt;**3. Ownership: **the owning team is assigned within 24 hours. No owner, no quarantine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Time limit:&lt;/strong&gt; 14 days. Fixed, deleted, or escalated at expiry.&lt;/p&gt;

&lt;p&gt;**5. Cap: **quarantine holds no more than 2% of the suite. At the cap, stabilization work takes priority over new test development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Visibility:&lt;/strong&gt; quarantined tests run nightly and report to the team channel.&lt;/p&gt;

&lt;p&gt;In GitHub Actions, run quarantined tests in a separate non-blocking job:&lt;/p&gt;

&lt;p&gt;jobs:&lt;/p&gt;

&lt;p&gt;e2e:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;runs-on: ubuntu-latest

steps:

  - uses: actions/checkout@v4

  - run: npx playwright test --grep-invert @quarantine

  - uses: actions/upload-artifact@v4

    if: always()

    with:

      name: playwright-report

      path: playwright-report/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;quarantined:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;runs-on: ubuntu-latest

continue-on-error: true

steps:

  - uses: actions/checkout@v4

  - run: npx playwright test --grep @quarantine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The continue-on-error: true flag keeps these results reported without blocking the merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key metrics teams should monitor
&lt;/h2&gt;

&lt;p&gt;Stabilization work needs numbers, or you cannot tell progress from luck.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5mf2lfqwclwzf9rg8dyq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5mf2lfqwclwzf9rg8dyq.png" alt=" " width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Track flake rate per spec alongside the suite-wide number. Averages hide the handful of tests doing the damage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do and Don't
&lt;/h2&gt;

&lt;p&gt;Do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Classify root cause before writing a fix&lt;/li&gt;
&lt;li&gt;Rank by flake rate multiplied by run frequency&lt;/li&gt;
&lt;li&gt;Seed data through the API, unique per test&lt;/li&gt;
&lt;li&gt;Store traces and video on first retry&lt;/li&gt;
&lt;li&gt;Report retry passes as flaky, never as green&lt;/li&gt;
&lt;li&gt;Put an expiry date on every quarantined test&lt;/li&gt;
&lt;li&gt;Pin browser and driver versions in CI&lt;/li&gt;
&lt;li&gt;Review the flakiest five specs in your weekly QA sync&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rewrite the suite before diagnosing it&lt;/li&gt;
&lt;li&gt;Add sleeps to "make it stable"&lt;/li&gt;
&lt;li&gt;Raise global timeouts as a blanket fix&lt;/li&gt;
&lt;li&gt;Retry entire pipeline jobs to get green&lt;/li&gt;
&lt;li&gt;Mix implicit and explicit waits in Selenium&lt;/li&gt;
&lt;li&gt;Share user accounts or records across parallel tests&lt;/li&gt;
&lt;li&gt;Skip tests silently with no owner or date&lt;/li&gt;
&lt;li&gt;Judge progress on suite-wide averages&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to start on Monday
&lt;/h2&gt;

&lt;p&gt;Pull the last 30 days of CI results. Rank specs by flake rate times run frequency. Take the top five, classify each against the root cause table, and fix by category. Quarantine anything you cannot fix in two weeks, with an owner and a date.&lt;/p&gt;

&lt;p&gt;In the audits we run, pipeline trust usually recovers from this alone, before anyone touches framework choice. If you would rather have an outside read on which tests to fix, quarantine, or delete, that is what &lt;a href="https://www.qamadness.com/services/test-automation-consulting-services/" rel="noopener noreferrer"&gt;test automation consulting services&lt;/a&gt; cover. In this case, experts audit the existing framework, stabilize what is worth keeping, and hand back the flake metrics and quarantine policy your team runs afterwards.&lt;/p&gt;

&lt;p&gt;By the way, if you have a stabilization tactic that works on your pipeline, drop it in the comments, it would be nice to read your experience. &lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
