<?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>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>
