<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Shyamal Raju on Medium]]></title>
        <description><![CDATA[Stories by Shyamal Raju on Medium]]></description>
        <link>https://medium.com/@shyamalraju?source=rss-80ddbfc1b64d------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*M8ifW-vTPR4Q7_VtFIpKew.jpeg</url>
            <title>Stories by Shyamal Raju on Medium</title>
            <link>https://medium.com/@shyamalraju?source=rss-80ddbfc1b64d------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 07:58:12 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@shyamalraju/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Rethinking performance testing for the modern web]]></title>
            <link>https://medium.com/refluent/rethinking-performance-testing-for-the-modern-web-with-browsers-338d0b84cf74?source=rss-80ddbfc1b64d------2</link>
            <guid isPermaLink="false">https://medium.com/p/338d0b84cf74</guid>
            <category><![CDATA[test-automation]]></category>
            <category><![CDATA[playwright-test]]></category>
            <category><![CDATA[performance-testing]]></category>
            <category><![CDATA[load-testing]]></category>
            <dc:creator><![CDATA[Shyamal Raju]]></dc:creator>
            <pubDate>Sun, 01 Mar 2026 11:24:17 GMT</pubDate>
            <atom:updated>2026-03-07T00:14:51.732Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2ey5mkxfRuFYWfuERQa1Fg.png" /></figure><p>Most people don’t call software slow because they’ve measured them against a benchmark. They call it slow because it <em>feels</em> slow.</p><p>Users experience this in a few familiar ways. It could be a button that doesn’t respond. Or the page reacts a beat late when you’re typing, scrolling, or clicking around.</p><p>The catch is, performance tests can miss that experience, because it has traditionally focused on the backend. Traditional load testing, for example, simulates users by sending requests, then measures response times and error rates from the server. For modern web apps, that often misses a lot of what happens in the browser.</p><p>Modern web apps don’t always “finish” when the server responds. After the first response arrives, the browser is often still working: loading assets, running scripts, fetching more data, and rendering the interface. In many apps, this browser work is where the delays are. The API can be fast, while the UI still feels stuck.</p><p>If performance is experienced in the browser, a complete performance test needs to measure the browser too.</p><h3>Where traditional load tests fall short</h3><p>Most performance testing starts with a simple idea: simulate real usage at scale, and see how the application responds.</p><p>In practice, that usually translates to load testing. We’d ramp up backend calls as we increased the number of users, and watch what happens to response times and error rates. This would give us a clear view of server capacity. It also tends to be repeatable, which makes it useful for baselining and comparing changes over time.</p><p>The catch is that modern web apps aren’t defined by a single request and response. A single user action can trigger a chain of work across the client (the browser) and the server. The browser loads assets, runs scripts, fetches more data, updates the UI, and only then becomes properly usable.</p><p>A lot of this work happens <em>asynchronously</em>. The app doesn’t do one step, wait, then do the next. It kicks off multiple bits of work in the background, and the page updates as each piece finishes. Some tasks run in parallel. Some depend on earlier results and some only happen after the first render.</p><p>The first animation below shows what this looks like in practice: one user action, like a button click, can quickly cascade into work across the client and server. In this example, the time to usability is ~2.5 seconds, and time to stability is ~3.5 seconds.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cWP5vxxLHJ5Re99gcYsaUQ.gif" /><figcaption><strong>A single user action often triggers chained browser work</strong></figcaption></figure><p>If we try to model the above user action faithfully with load tests, the task becomes complex very quickly. You may need to simulate dozens, sometimes hundreds, of requests, and link and parameterise them realistically so the chain behaves realistically. In practice, that means accounting for asset loading, incidental browser traffic, and third-party activity as well.</p><p>This is why traditional performance testing tends to rely on abstraction.</p><p>The next animation shows what that abstraction looks like in practice. It starts with recording the full cascade of activity, then strips it back by discarding browser-side work and any requests the test isn’t modelling. What’s left is a smaller set of backend interactions that can be executed reliably under load.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Nih8lhj-EqbMyPjmi9XmUA.gif" /><figcaption><strong>A traditional load test collapses the flow to key endpoints</strong></figcaption></figure><p>In our example, the abstracted load test might report the time taken as under 1 second, because it’s only measuring the selected backend interactions.</p><p>So traditional performance tests often narrow the focus to a small set of “important” endpoints. This requires judgement calls about what to keep and what to discard. And often, it is easy to overlook and discard something that’s discovered to be useful later.</p><p>But this over-simplification is also deliberate for a few more reasons:</p><ul><li><strong>Stability</strong> — fewer moving parts means fewer flaky results caused by background calls, caching, or client-side timing.</li><li><strong>Interpretability</strong> — it’s easier to explain a spike in one endpoint than to untangle fifty small calls that all moved a bit.</li><li><strong>Cost</strong> — running and maintaining large, browser-faithful scripts takes time, and at scale it can be expensive.</li></ul><p>For a long time, that trade-off made sense. You got fast tests, clean metrics, and a reliable view of server behaviour under load.</p><p>But the abstraction also hides things users actually see and feel. API-level tests won’t see client-side execution, rendering delays, long tasks, and blocked interactions. The UI can feel slow even when no single API call looks terrible. A test can report a fast response time while users are still waiting for the interface to become usable. In short, we’ve already abstracted away what users <em>experience</em> in the browser.</p><p>Seen this way, traditional performance testing isn’t wrong. It’s just incomplete. It tells you how the server behaves under load, but not how the application behaves for the user. As more execution shifts into the browser, that distinction matters more.</p><p>So what does performance testing look like when we stop abstracting away what users experience in the browser?</p><h3>A broader view of performance testing</h3><p>If API-driven performance testing tells us how the server behaves under load, it says far less about how the system behaves for the user. Closing that gap means expanding performance testing beyond the API layer and into the browser.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*E4W8jz1-gM88dQsY6lZoIg.png" /><figcaption><strong>browser-driven performance testing extends the boundary to the client</strong></figcaption></figure><p>Browser-driven performance testing starts from the user’s point of view. First, we have to script user interactions through the UI as end-to-end tests do: navigate, click, submit, and wait for state. Then we scale those user flows under load, so we can measure performance across the full path, including the client and the server, rather than isolating individual endpoints.</p><p>This changes the kinds of issues performance testing can surface. Instead of focusing only on response times, we can see how long it takes for a page to become usable, how responsive the interface feels under load, and where delays actually emerge. Problems that are invisible at the API layer — like rendering stalls, long-running scripts, chained requests, and blocked interactions — become visible once the browser is part of the test.</p><p>The point isn’t that backend testing is useless. It’s that modern applications do meaningful work in the browser, and that work shapes the experience. If we don’t measure it, we’re not accounting for a crucial part of the system.</p><p>Browser-driven performance testing brings measurement closer to what it’s meant to reflect: how the product behaves from the user’s point of view. And by leveraging end-to-end tests performed in the browser, it also solves for the complex tasks of modelling the entire cascade of work performed in the client and the server.</p><p>But at this point you might be thinking: this sounds good in theory, but does it actually work at scale?</p><h3>Browser-driven performance testing in practice</h3><p>Moving performance testing into the browser has becoming increasingly simple with modern tooling. One practical approach is to use an end-to-end automation framework like <strong>Playwright</strong> to describe real user interactions, then run those scenarios at scale using a load engine like <strong>Artillery</strong>.</p><p>We’d start by capturing a user flow with a Playwright script. For example, a simple login and dashboard load might look like this:</p><pre>export async function loginAndLoadDashboard({ page }) {<br>  await page.goto(&quot;https://example.app/login&quot;);<br>  await page.fill(&quot;#username&quot;, userEmail);<br>  await page.fill(&quot;#password&quot;, userPassword);<br>  await page.click(&quot;button[type=submit]&quot;);<br>  await page.waitForSelector(&quot;#dashboard&quot;);<br>}</pre><p>To turn that journey into a performance test, it needs to be executed repeatedly under controlled conditions. This is where load generation with Artillery comes in.</p><p>Artillery orchestrates and measures the Playwright tests at scale. It controls how many browser sessions run, how users arrive over time, and how timings are collected and aggregated. Because browser sessions are compute-heavy, Artillery spreads execution across multiple machines. This ensures the test is limited by the system under test, not the machine running the test.</p><p>Once a Playwright test has been written, configuring load generation is surprisingly simple with Artillery. Here’s what an Artillery configuration YAML might look like:</p><pre>config:<br>  target: &quot;https://ourAwesome.app&quot;<br>  phases:<br>    - duration: 300<br>      arrivalRate: 5<br>  engines:<br>    playwright: {}<br><br>scenarios:<br>  - engine: playwright<br>    testFunction: &quot;loginAndLoadDashboard&quot;</pre><p>The responsibilities are deliberately split. The browser automation defines what the user does. The load generator defines how often it happens, at what scale, and how results are aggregated and measured. This separation keeps user scenarios readable and reusable, while allowing load shape, scale, and distribution to be tuned independently.</p><p>There are other alternatives to this tool stack as well. Some teams extend existing load testing tools with browser execution, while others combine different automation frameworks and runners. The specific tools matter less than the approach. What defines browser-driven performance testing is the decision to measure performance where users actually experience it: in the browser, executing real behaviour, under realistic load.</p><p>But of course, nothing comes for free. The next obvious question is what you gain, and what you trade off, by doing performance testing in the browser.</p><h3>The trade-offs</h3><p>Browser-driven performance testing is more realistic, because it simulates the end-to-end flows users run. But in return, scaling browser tests requires more compute.</p><p>In theory this should be more expensive. But in reality, this option is far more accessible in the current tooling landscape. With modern headless browsers, better orchestration, and the option to run on self-managed infrastructure, browser-based tests can be run with predictable cost. In many cases, this ends up being cost-competitive with (and, in my experience, even cheaper than) traditional load testing tools.</p><p>The developer experience has improved too. Tools like Artillery + Playwright let you keep scenarios close to the functional tests teams already know, while keeping load shape, scale, and distribution in a separate configuration that’s easy to tune.</p><h3>Wrapping up</h3><p>Performance testing has always aimed to answer a simple question: is the app usable at real-world scale?</p><p>For a long time, the best way to answer that was to stress the server and measure response times. That’s still useful as it tells you a lot about the application capacity and stability. But it’s only one part of the picture.</p><p>Traditional load testing tools tended to rely on abstraction. They simplify real user behaviour down to a smaller set of backend interactions. That makes tests repeatable, but it can miss issues that only show up in the browser.</p><p>Browser-driven performance tests close that gap. They run real user flows in the browser under load, capturing both client-side behaviour and server-side signals.</p><p>The end result is a more complete view of performance: how the backend holds up under load, and how the product behaves for users at scale.</p><p><strong><em>Note</em></strong><em>: This post is independent. It is not endorsed or sponsored by any company and product mentioned.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*T1RRFuKkPm09HK6H.png" /></figure><p><strong><em>If you enjoyed this article,</em></strong><em> please join my substack newsletter — </em><a href="http://blog.refluent.co/"><strong><em>refluent</em></strong></a><strong><em>. </em></strong><em>You’ll also be notified when the next article is published.</em></p><p><em>Refluent shares sharp insights on quality engineering, performance testing, and the future of intelligent automation.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=338d0b84cf74" width="1" height="1" alt=""><hr><p><a href="https://medium.com/refluent/rethinking-performance-testing-for-the-modern-web-with-browsers-338d0b84cf74">Rethinking performance testing for the modern web</a> was originally published in <a href="https://medium.com/refluent">refluent</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to implement self-healing tests with AI]]></title>
            <link>https://medium.com/refluent/how-to-implement-self-healing-tests-with-ai-640b0c8139a4?source=rss-80ddbfc1b64d------2</link>
            <guid isPermaLink="false">https://medium.com/p/640b0c8139a4</guid>
            <category><![CDATA[self-healing]]></category>
            <category><![CDATA[test-automation]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[quality-assurance]]></category>
            <category><![CDATA[artifical-intellegence]]></category>
            <dc:creator><![CDATA[Shyamal Raju]]></dc:creator>
            <pubDate>Tue, 26 Aug 2025 20:50:21 GMT</pubDate>
            <atom:updated>2026-02-11T01:15:17.025Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BN9DlcJFMb5Uwj2lfuwCtg.png" /></figure><p>Self-healing has long been one of the toughest challenges in software test automation. Automated tests are meant to catch real issues, but they often break when an application changes unexpectedly. Over time, these failures pile up, shifting teams from building new tests to endlessly patching old ones.</p><p>This is the paradox of test automation: it promises stability, yet it demands constant repair.</p><p>In theory, self-healing can ease this burden by allowing tests to adapt automatically to application changes. In practice, though, self-healing has been difficult to achieve. Traditional self-healing approaches rely on rigid, rule-based logic that struggles to keep pace with evolving applications and often requires intrusive adjustments to the codebase itself.</p><p>But recent advances in artificial intelligence are changing that. Large language models (LLMs), such as the ones powering ChatGPT, offer a way to reason about context and intent, making adaptive, context-aware self-healing a realistic goal.</p><p>This article will explore what self-healing is, why it matters, and how LLMs can enable it. We’ll also work through a high-level example of implementing self-healing from scratch. We’ll break down the challenges of self-healing and discuss the fundamental building blocks of an AI-assisted self-healing system.</p><h3>What exactly is self-healing?</h3><p>One of the most frustrating parts of test automation is when a test fails for the <em>wrong</em> reason. An updated feature may be working perfectly, but the test breaks because something minor changed, such as a label, an ID, or the shape of a response.</p><p>Consider a simple case. Suppose you have a test that expects a button labelled Save. A developer renames it to Submit, but everything else still works exactly the same. This causes the test to fail, not because the feature is broken, but because it was written to look for the old label.</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/0*owiFTTetNG9gITnr.png" /><figcaption>Example of an error following a Save-Submit button label change</figcaption></figure><p>This brittleness is everywhere. Automated tests are often tightly coupled to specific implementation details, so harmless changes like this can trigger failures. The Save→Submit example shows a UI change, but the same problem appears at every layer: a renamed input in unit tests, a restructured API response in integration tests, or an altered DOM layout in end-to-end tests.</p><p>Self-healing, or auto-healing, is the idea of breaking this cycle. A self-healing system should detect a harmless change and adjust the test automatically. In the Save→Submit case, it would recognise the new label, update the selector, and continue running without human intervention. Tests stay in sync with the application, and the burden of maintenance is reduced.</p><p>That may sound simple, but in practice, it isn’t. Let’s look at why.</p><h3>Why is self-healing tough?</h3><p>Self-healing is challenging because not every test failure means the same thing. Some failures signal a genuine defect in the application, while others reflect a test that’s simply out of date. To tell the difference, you need to understand what the test is verifying, how it’s written, and whether the application’s new behaviour still aligns with that intent. In short: you need context.</p><p>To understand why context matters, let’s look at the kinds of changes that typically break tests:</p><ul><li><strong>Semantic changes</strong>: updates to naming or labels of objects, like renaming a button from <em>Save</em> to <em>Submit</em>.</li><li><strong>Structural changes</strong>: shifts in layout or placement, like moving a button to another part of a form.</li><li><strong>Behavioural changes</strong>: modifications to the logic or flow, like altering when a button can be clicked or what action it performs.</li></ul><p>Any one of these changes, or a combination of them, can cause a cascade of test failures even when the application still works as expected. Writing rules to handle all these cases in a clean and flexible way is difficult. Without the contextual understanding that LLMs bring, the result is often rigid with overly complex logic. In some cases, the application ends up being adapted to fit the tests, rather than the tests adapting to the application.</p><p>This is where LLMs can help. By interpreting context and meaning more flexibly, they offer a more adaptable way to keep tests aligned with the application as it evolves. With this in mind, let’s quickly examine the advantages and limitations of using LLMs for self-healing.</p><h3>Self healing with LLMs</h3><p>Large language models, such as the GPT family, make self-healing easier by reasoning about the intent and context of a test, rather than relying on complex hardcoded rules. At their core, LLMs are trained on vast amounts of text and code, which gives them the ability to “fill in the blanks”. They can infer meaning from partial information and suggest likely continuations. In practice, that means they can often detect what a test is trying to achieve and propose a fix when it breaks.</p><p>But LLMs bring their own challenges. The first challenge is <strong>context management</strong>. With too little context, an AI model doesn’t have enough information to make a good decision. With too much, the output becomes unreliable, slow, or costly. Giving the model just enough detail to be useful without overwhelming it, is a key design consideration when building a reliable self-healing system.</p><p>The second challenge is <strong>hallucination</strong>. A model may suggest selectors, attributes, or code changes that don’t exist. Worse, it has no real way of knowing it’s wrong. Left unchecked, these “fixes” can create tests that pass for the wrong reasons or fail in new ways — the very problem we’re trying to solve.</p><p>We can mitigate these challenges by adding guardrails in a well-designed self-healing system. By grounding model suggestions in real application data and carefully managing context, we can take advantage of what LLMs do well while reducing the risk of noisy context or hallucinations. But, the real question is how to bring these ideas together into a system that actually works. That’s what we’ll look at next.</p><h3>Designing a good self-healing system</h3><p>A reliable LLM-powered self-healing system rests on three core stages: <strong>Context Composition, LLM Evaluation, and Validation</strong>. Together these stages form a feedback loop that diagnoses the cause of a test failure, proposes a fix, and validates the fix before accepting it. This loop is designed to address the two main challenges of working with LLMs: managing context effectively and reducing the risk of hallucinations.</p><p>The diagram below shows a simplified “happy path” workflow. It illustrates how a harmless application change, such as a renamed button label, is detected and corrected with the help of an LLM. While real-world self-healing systems will require more logical layers (such as handling a true positive or handling incorrect fixes), this example captures the foundational building blocks you‘ll need before developing for more complex cases.</p><p>What’s striking about this three-stage loop is its generality. The pattern isn’t limited to self-healing tests; it’s a practical approach to making AI-incorporated systems more reliable. LLMs remain probabilistic, but the loop of context, evaluation, and validation forces their outputs into a process we can check and control.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uwGl0UXKa8sEdfo9ej-rjQ.png" /><figcaption>A simplified AI-assisted self-healing workflow</figcaption></figure><h3>Practice example</h3><p>To see how the above flow works in practice, let’s use a simple example with the <a href="https://todomvc.com/">TodoMVC</a> app and Playwright (but you won’t need to be deeply familiar with Playwright to follow along).</p><p>Here’s a test that tries to add a new todo item by filling an input placeholder called What needs to be done:</p><pre>test(&#39;adds a new todo item&#39;, async ({ page }) =&gt; {<br>  await page.goto(&#39;https://todomvc.com/examples/react/dist/&#39;);<br>  await page.getByPlaceholder(&#39;What needs to be done?&#39;).fill(&#39;Buy milk&#39;);<br>  await page.getByPlaceholder(&#39;What needs to be done?&#39;).press(&#39;Enter&#39;);<br>  await expect(page.getByText(&#39;Buy milk&#39;)).toBeVisible();<br>});</pre><p>Now imagine the placeholder changes to Add new task. The functionality is the same, but the test fails because it was written for the old label. This is where our three-stage loop comes in: <strong>Context Composition → LLM Evaluation → Validation</strong>.</p><h4>1. Context Composition</h4><p>The process begins when a test fails in a continuous integration (CI) pipeline. In this case, Playwright throws an error indicating the label it was looking for could not be found:</p><pre>Error: Strict mode violation: locator(&quot;placeholder=What needs to be done?&quot;) resolved to 0 elements.</pre><p>That failure triggers the self-healing loop. The pipeline captures relevant details about the failure:</p><pre>{<br>  &quot;testName&quot;: &quot;adds a new todo item&quot;,<br>  &quot;testFile&quot;: &quot;tests/todomvc.spec.ts&quot;,<br>  &quot;failingStep&quot;: &quot;page.getByPlaceholder(&#39;What needs to be done?&#39;)&quot;,<br>  &quot;errorMessage&quot;: &quot;Strict mode violation: locator(&#39;placeholder=What needs to be done?&#39;) resolved to 0 elements&quot;,<br>  &quot;errorLocation&quot;: 50<br>  &quot;domSnapshot&quot;: &quot;&lt;input placeholder=&#39;Add a new task&#39; class=&#39;new-todo&#39;&gt; ...&quot;,<br>  &quot;testFile&quot;: &quot;test(&#39;adds a new todo item&#39;, async ({ page }) =&gt; { ...&quot;<br>}</pre><p>Good context filters out noise so the LLM focuses only on high-value information. This directly addresses the challenge of managing context effectively.</p><h4>2. LLM Evaluation</h4><p>Next, the above context is sent to an LLM, such as openAI’s GPT4, via an API. The request will have two parts:</p><ul><li>A <strong>system prompt</strong> with instructions directing an AI model how to behave:</li></ul><pre>{<br>  &quot;role&quot;: &quot;system&quot;,<br>  &quot;content&quot;: &quot;You are an automated test repair assistant for Playwright (TypeScript). Only adjust selectors - no business logic changes.&quot;<br>}</pre><ul><li>A <strong>user prompt</strong> with the failure context:</li></ul><pre>{<br>  &quot;role&quot;: &quot;user&quot;,<br>  &quot;content&quot;: {<br>    &quot;context&quot;: {<br>      &quot;testName&quot;: &quot;adds a new todo item&quot;,<br>      &quot;failingStep&quot;: &quot;page.getByPlaceholder(&#39;What needs to be done?&#39;)&quot;,<br>      &quot;errorMessage&quot;: &quot;Strict mode violation: locator(&#39;placeholder=What needs to be done?&#39;) resolved to 0 elements&quot;,<br>      &quot;domSnapshot&quot;: &quot;&lt;input placeholder=&#39;Add a new task&#39; class=&#39;new-todo&#39;&gt; ...&quot;,<br>      &quot;testFile&quot;: &quot;// tests/todo.spec.ts ...&quot;<br>     },<br>    &quot;task&quot;: &quot;Propose minimal edits to fix the selector while preserving test intent. Respond as JSON with `rationale` and `changes`.&quot;<br>  }<br>}</pre><p>The LLM analyses the context and proposes a targeted fix:</p><pre>{<br>  &quot;analysis&quot;: &quot;The test expects the placeholder &#39;What needs to be done?&#39;, but the DOM shows &#39;Add a new task&#39;.&quot;,<br>  &quot;fix&quot;: {<br>    &quot;file&quot;: &quot;tests/todo.spec.ts&quot;,<br>    &quot;line&quot;: 50,<br>    &quot;column&quot;: 7,<br>    &quot;oldCode&quot;: &quot;await page.getByPlaceholder(&#39;What needs to be done?&#39;).fill(&#39;Buy milk&#39;);&quot;,<br>    &quot;newCode&quot;: &quot;await page.getByPlaceholder(&#39;Add a new task&#39;).fill(&#39;Buy milk&#39;);&quot;,<br>    &quot;reason&quot;: &quot;The selector needs to be updated to reflect the new placeholder.&quot;<br>  }<br>}</pre><h4>3. Validation</h4><p>Finally, the proposed fix is validated in the CI pipeline:</p><ol><li>Apply the changes on a temporary branch.</li><li>Re-run the failing test.</li><li>Confirm that it now passes.</li></ol><p>If validation succeeds, the system raises a pull request for human review:</p><pre>{<br>  &quot;title&quot;: &quot;Self-healing update - fix placeholder selector in todo.spec.ts&quot;,<br>  &quot;body&quot;: &quot;Updated placeholder from &#39;What needs to be done?&#39; to &#39;Add a new task&#39; based on DOM snapshot analysis. Validated in CI.&quot;,<br>  &quot;branch&quot;: &quot;self-heal/update-placeholder&quot;,<br>  &quot;base&quot;: &quot;main&quot;<br>}</pre><p>Together, the whole workflow shows how the self-healing loop takes a brittle test, diagnoses the cause, proposes a fix, and ensures the change is safe before it is accepted.</p><h3><strong>Wrapping up</strong></h3><p>Thanks for following along. If some of these ideas feel new, they’ll quickly become familiar as you spend more time with AI-driven automation.</p><p>We looked at what self-healing is, why it has been so hard to achieve, and how LLMs can change the equation. We sketched out a three-stage design that lays the groundwork for moving into implementation: Context Composition, LLM Evaluation, and Validation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AHLofSZWw3OlTsUH8SNF_Q.png" /></figure><p><strong>If you enjoyed this article,</strong> please join my substack newsletter — <a href="http://blog.refluent.co/"><strong>refluent</strong></a><strong>. </strong>You’ll also be notified when the next article is published.</p><p>Refluent shares sharp insights on quality engineering, performance testing, and the future of intelligent automation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*r-rXAaDo96v_KXZUOCNO3w.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=640b0c8139a4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/refluent/how-to-implement-self-healing-tests-with-ai-640b0c8139a4">How to implement self-healing tests with AI</a> was originally published in <a href="https://medium.com/refluent">refluent</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The hidden costs of conditional testing in test automation]]></title>
            <link>https://medium.com/refluent/the-hidden-costs-of-conditional-testing-in-ui-automation-cb766448c638?source=rss-80ddbfc1b64d------2</link>
            <guid isPermaLink="false">https://medium.com/p/cb766448c638</guid>
            <category><![CDATA[conditional-statements]]></category>
            <category><![CDATA[quality-assurance]]></category>
            <category><![CDATA[test-automation]]></category>
            <dc:creator><![CDATA[Shyamal Raju]]></dc:creator>
            <pubDate>Wed, 13 Mar 2024 11:08:59 GMT</pubDate>
            <atom:updated>2025-12-12T22:12:54.040Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*qzFKX7VKN2TOBvawtP5a7w.png" /></figure><p>Conditional testing, at its core, seems like a straightforward and logical approach to automating tests: write conditions, and based on those conditions, execute specific actions. This method promises a level of adaptability and intelligence in handling various scenarios in automated tests. However, this common test pattern often conceals a tangled web of challenges.</p><p>Despite the apparent simplicity of conditional testing they often lead to unpredictable outcomes and complex test maintenance. What’s more, conditional tests can mask these problems until they appear much later, leading to costly and time-consuming fixes.</p><p>In this article, we’ll peel back the layers of conditional testing in test automation, revealing why it’s often considered an anti-pattern and how steering clear of it can lead to more resilient and maintainable test suites.</p><h4>What are conditional tests?</h4><p>Conditional tests use conditional logic to test diverging pathways in software applications. You will know them by the use of familiar if-else statements:</p><blockquote><strong><em>if</em> </strong>x <strong><em>then</em></strong><em> </em>y <strong><em>else</em></strong> z</blockquote><p>In test automation, they are commonly applied to <em>initialise the state</em> of the application, as well as for <em>testing diverging pathways. </em>But as we’ll soon discover, they can also lead to some unintuitive consequences.</p><p>Shown below is a basic end-to-end test scenario that uses conditional testing to validate an app’s dark mode. This is an oversimplified example, but it should help us understand some key aspects of conditional testing:</p><pre>1. Select a random user from the test database.<br>2. Navigate to the &#39;Settings&#39; page.<br>3. Check that &#39;Dark mode&#39; is enabled:<br>  A: If dark mode is enabled, skip to step 4.<br>  B: If dark mode is disabled, toggle on dark mode.<br>4. Verify dark mode.</pre><p>The test design is also illustrated below. The decision point to check whether dark mode is enabled is depicted in yellow, and the two logical pathways are depicted in blue and green. This design helps ensure 1) the required software state is initialised for each path to execute predictably, and 2) diverging pathways for either state can be tested.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vC_zr-wfoybX9i8Je6ioUA.png" /><figcaption>Conditional test for validating dark mode.</figcaption></figure><p>At first glance, this design seems effective for initialising the application state and testing diverging pathways. However, the very nature of diverging logic can produce some unintuitive issues which we’ll turn to next.</p><h4>Problems with conditional tests</h4><p>The diverging logic in conditional testing can be particularly problematic when paired with randomised data to drive the test. In most applications, the shape and distribution of each dataset will likely be different, which can lead to the following:</p><ul><li><strong>Unpredictable test frequency</strong>: The number of times each pathway is tested is dependent on the test data distribution, which can vary greatly. For example, if only 1 in 5 users have dark mode disabled as illustrated below, the blue path for enabling dark mode would only execute 20% of the time.</li><li><strong>Flaky edge cases</strong>: The unpredictable schedule of tests can naturally lead to flaky tests. If changes in the application result in test failures for any conditional pathways, the test failures might show up as infrequent edge cases, leading to unreliable test results. This is particularly problematic for test failures caused by bugs, which may not be picked up immediately.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6oyLkR61EjiCcS4grx2uaQ.png" /><figcaption>Conditional tests can produce unpredictable test frequencies and flaky edge cases when paired with random data.</figcaption></figure><p>While the above issues tend to occur with randomised data, conditional tests can introduce additional layers of complexity even with non-randomised data:</p><ul><li><strong>Synchronisation and awaits</strong>: Conditional tests often require additional handling to ensure that the application is in the expected state before proceeding. If not carefully implemented, this can lead to increased test execution time or race conditions that cause intermittent failures and flaky tests.</li><li><strong>Difficult to debug</strong>: Due to the tangling logic of multiple pathways and diverging application states, conditional tests tend to be unintuitive and difficult to interpret. This can make debugging tedious and time-consuming.</li><li><strong>Reproducibility</strong>: A conditional test may take a different pathway each time it is run. Hence, reproducing errors in a test can require multiple test runs, manual manipulation of application states or mocking of variables to trigger specific test pathways.</li></ul><p>These pitfalls can produce unreliable results and excessive maintenance overheads. What’s more, due to the unpredictable test frequencies and edge cases, these problems could appear much later, leading to costly and time-consuming fixes.</p><p>Now that we have an understanding of the problems that conditional tests introduce, let’s look at some solutions for navigating conditional tests.</p><h4>Solving for conditional tests</h4><p>To avoid the pitfalls of conditional testing we have to write more deterministic tests. In practice, this means designing tests with <strong>setups for initialising application states</strong> while using<strong> linear logic</strong>.</p><p>Let’s revisit our dark mode test as an example. In the dark mode test, we started by fetching a random user from the database, which left us at the whim of random application states (either dark mode or light mode). This is what the query for the fetch would have looked like:</p><pre>SELECT user_id FROM users<br>ORDER BY RANDOM()<br>LIMIT 1;</pre><p>Instead of using conditional logic to set the application state via the UI, we can initialise predictable application states right at the start of our tests. We’ll ensure only data for users that have dark mode disabled are fetched at the start of the test (alternatively, we could <em>set</em> the dark mode for <em>any</em> user to true):</p><pre>SELECT u.user_id, us.notifications_enabled<br>FROM users u<br>JOIN user_settings us ON u.user_id = us.user_id<br>WHERE us.notifications_enabled = true<br>ORDER BY RAND()<br>LIMIT 1;</pre><p>Now that we know that the dark mode toggle will always be off, we can remove the conditional pathways in our script. This allows for a linear test flow:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bcLXmFYfIJ-I6RhmfPpYSQ.png" /><figcaption>Validating dark mode without conditional statements.</figcaption></figure><p>It is worth noting that we could use other setup methods to initialise the application state early, such as seeding the application with data before each test. In either case, removing conditional statements in test design simplifies test logic, making tests more consistent, more reliable, and easier to maintain. However, it’s important to recognise that conditional statements aren’t inherently problematic and can be useful in some occasions.</p><h4>When should you use conditional statements?</h4><p>Of course, conditional statements can be useful in automation tests but their use should be carefully considered. When needed, it’s crucial to implement them in a way that minimises their impact on test predictability and maintainability:</p><ul><li><strong>Use conditional logic sparingly</strong>: Reserve conditional logic for scenarios where it’s absolutely necessary. For example, handling different user roles that have access to different features in the application, or for use in polling functions that wait for an application state.</li><li><strong>Isolate conditional logic</strong>: When possible, isolate conditional logic to specific parts of the test, particularly the setup phase. This should make it easier to identify and manage conditional logic.</li><li><strong>Document thoroughly</strong>: Ensure that any conditional logic within your tests is well-documented to explain why it’s needed and how it affects the test flow. This documentation will be invaluable for maintenance and debugging.</li></ul><p>With test automation, and especially UI automation, conditional statements should be the exception, not the rule. The key concern is not to trade off determinism in a test unnecessarily.</p><h4>Final Thoughts</h4><p>Conditional testing presents a paradox: while seemingly useful and flexible, it introduces complexity, unpredictability, and maintenance challenges. By recognizing the hidden costs associated with conditional tests, automation testers can take proactive steps to minimize their use, simplifying test logic and improving reliability.</p><p>Adopting more deterministic testing practices, such as writing linear tests and initializing the application state early, can significantly reduce the reliance on conditional logic. When conditions are necessary, applying best practices to manage their complexity ensures that tests remain as predictable and maintainable as possible.</p><p>Ultimately, the goal of any testing strategy should be to create a suite of tests that are reliable, easy to understand, and straightforward to maintain. By carefully considering the role of conditional logic in your tests, you can achieve a balance between flexibility and predictability, leading to a more reliable testing process.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AHLofSZWw3OlTsUH8SNF_Q.png" /></figure><p><strong>If you enjoyed this article,</strong> please join my substack newsletter — <a href="http://blog.refluent.co/"><strong>refluent</strong></a><strong>.<br></strong>We’ll notify you when the next article is published.</p><p>Refluent shares sharp insights on quality engineering, performance testing, and the future of intelligent automation.</p><p>You can also find me on <a href="https://twitter.com/shyamal_tweets">X</a> and <a href="https://www.linkedin.com/in/shyamalraju/">LinkedIn</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*r-rXAaDo96v_KXZUOCNO3w.png" /></figure><h3>References</h3><p>I found the following resources on conditional testing invaluable:</p><ol><li>An excellent explainer on the pitfalls of conditional testing on Cypress docs: <a href="https://docs.cypress.io/guides/core-concepts/conditional-testing">Conditional Testing</a>.</li><li>A good short article by Ethan Carlsson: <a href="https://dev.to/ethancarlsson/dont-write-conditional-logic-in-tests-5aa">Don’t write Conditional Logic in Tests</a>.</li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cb766448c638" width="1" height="1" alt=""><hr><p><a href="https://medium.com/refluent/the-hidden-costs-of-conditional-testing-in-ui-automation-cb766448c638">The hidden costs of conditional testing in test automation</a> was originally published in <a href="https://medium.com/refluent">refluent</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Micro tests, macro impacts: the benefits of switching to Atomic testing]]></title>
            <link>https://medium.com/refluent/micro-tests-macro-impacts-the-benefits-of-switching-to-atomic-testing-dab8af25acbf?source=rss-80ddbfc1b64d------2</link>
            <guid isPermaLink="false">https://medium.com/p/dab8af25acbf</guid>
            <category><![CDATA[atomic-testing]]></category>
            <category><![CDATA[software-testing]]></category>
            <category><![CDATA[quality-assurance]]></category>
            <category><![CDATA[end-to-end-testing]]></category>
            <category><![CDATA[test-automation]]></category>
            <dc:creator><![CDATA[Shyamal Raju]]></dc:creator>
            <pubDate>Thu, 08 Feb 2024 09:41:38 GMT</pubDate>
            <atom:updated>2025-12-12T22:07:15.080Z</atom:updated>
            <content:encoded><![CDATA[<h3>Micro tests, macro impact: the benefits of switching to Atomic UI tests</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NxxhQ8DcJNdUYh6ubpnaSg.png" /></figure><p>Small tests can be surprisingly powerful in test automation, especially when compared to traditionally lengthier end-to-end tests. End-to-end tests, while invaluable for real-world test coverage, often has significant downsides. These tests tend to be slow, prone to errors, and often create scaling bottlenecks. To improve test performance, reliability, and scalability, we have to move beyond the constraints of end-to-end test automation.<br> <br>This is where a shift towards smaller, more targeted tests becomes crucial. Atomic UI tests, which are defined by their focused approach and isolated scope, challenge the constraints of end-to-end tests whilst retaining their benefits. They are a powerful response to end-to-end tests, but before we delve into Atomic tests further, let’s take a closer look at the problems they help solve.</p><h3>The problems with end-to-end tests</h3><p>End-to-end tests are useful for simulating real user scenarios, primarily through the UI, to validate the complete flow of an application from start to finish. Testing an application through the UI is invaluable for ensuring all application components and dependencies are working in sync. But automating end-to-end tests is also precisely problematic because of their over-reliance on the UI for testing.</p><p>Each additional UI interaction in an end-to-end test increases the test complexity and the likelihood of generating a test error. These errors can stem from various factors, including dynamic page content, timing and synchronisation issues, and unpredictable internal or external dependencies. Put simply, the longer an end-to-end test, the more likely it will break.</p><p>The likelihood of test errors due to the UI is also compounded by other common issues in end-to-end tests. We’ll outline some key problems that Atomic UI tests can solve below with the help of an example. Let’s start by considering an online store with some end-to-end test scenarios. We can experience the following problems with these tests:</p><ul><li><strong>Lengthy execution time</strong>: End-to-end tests often take a long time to run due to their comprehensive coverage of the entire application flow. In the test below called AddItemToCart, each <em>setup</em> step in the workflow (blue boxes) needs to be executed to perform the key action and validation steps (yellow boxes). The extensive workflow, which includes setup and cleanup steps performed via the UI, prolongs the total execution time of the test.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*h-9G3KUUgbeV9mCL_xrfGA.png" /><figcaption>End-to-End test: Long workflows extend the execution time of the test.</figcaption></figure><ul><li><strong>Sensitive to changes</strong>: End-to-end workflows can be highly sensitive to changes in more than one area of the application, often triggering a chain of failing steps. For example, a test failure in the ‘Select Product’ step below would prevent the key steps (in yellow) from executing. This makes the test needlessly brittle and prone to signalling false negative results.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Sy5lvFQvjrRQcxwTYDQyzw.png" /><figcaption>End-to-End test: Changes in one area of the application can affect the whole test</figcaption></figure><ul><li><strong>Lack of test isolation</strong>: Often many end-to-end tests share common setup steps which can trigger multiple test failures. For example, a test failure in the shared ‘Select Product’ step below would not only cause AddItemToCart to fail, but it will also fail unrelated tests as shown. This produces excessive noise, instead of accurately identifying the source of failures.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LYD93FLsRF1TWDeNa6hfyQ.png" /><figcaption>End-to-End test: Failures in shared steps often fail multiple tests.</figcaption></figure><p>Summing up, end-to-end tests have a wider surface area for UI interaction, resulting in tests that are slower to execute, more prone to failure, and often with inaccurate results. Though invaluable for confirming real-world functionality, these tests evidently require a more considered balance between coverage and reliability. Let’s unpack how Atomic tests help us achieve that.</p><h3>Shifting to Atomic UI tests</h3><p>Atomic UI tests are focused tests that cover a very specific software function. The term <em>Atomic</em> here implies the <em>smallest</em> <em>sensible</em> UI test possible to validate a piece of functionality. Atomic tests are somewhat contrary to end-to-end tests because Atomic tests are designed to be <strong><em>functionality-focused</em></strong>, <strong><em>minimal</em></strong>, and <strong><em>independent</em></strong>.</p><p>These three points can be regarded as the guiding principles for designing Atomic UI tests, but the specific techniques to implement them can vary; including the use of session cookies, browser javascript functions, application API calls, and backend/database operations. Let’s expand on the three key terms a little further with the previous AddItemToCart example:</p><ul><li><strong>Focused functionality</strong>: Each test is intended to be isolated and limited in scope to assess only the functionality under test. For example, The AddItemToCart test should be limited to the key actions and validations only, while abstracting away any other steps as much as possible. This should make test execution more efficient.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iicdkdBOij7b6wQbuNVQfg.png" /><figcaption>Atomic test: tests should be isolated to key steps.</figcaption></figure><ul><li><strong>Minimal setup and teardown</strong>: Atomic UI tests use minimised setups and teardowns, particularly by reducing UI interactions to focused areas as much as possible. For instance, we can reduce the UI interactions for AddItemToCart to key steps (in yellow) by abstracting away setup and teardown components with methods and operations that aren’t dependent on the UI (depicted by the grey boxes). The goal is to meaningfully isolate the UI test only to what is necessary and nothing more, resulting in more reliable tests.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZxWkmQW7_dYa5cS__b_8fQ.png" /><figcaption>Atomic test: UI test is isolated to key steps.</figcaption></figure><ul><li><strong>Test isolation</strong>: Atomic UI tests are intended to be run independently of each other. This ensures that the failure of one test does not impact the results of another. For example, if we were to split our earlier end-to-end test AddItemToCart, we’d have (at least) two independent tests with minimal external impacts. This also helps us identify the source of an error more efficiently, making test results more accurate.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zwnzBkFeYiUpCAdMW_nvXA.png" /><figcaption>Atomic test: Independent tests help isolate test failures and produce clearer test signals.</figcaption></figure><p>We can achieve greater test atomicity by abstracting away setup and teardown components with methods that do not depend on the UI. And by reducing the UI automation surface area, we can achieve faster execution, greater reliability, and higher accuracy. These gains also set the foundation for expanding our test coverage in a more scalable manner. Now that we understand the mechanisms and benefits of Atomic testing, let’s quickly tackle the question of how much atomicity is needed for a test to be considered Atomic.</p><h4>How atomic is atomic?</h4><p>Traditional end-to-end testing strategies often link comprehensive coverage with extensive, multi-step tests. Atomic testing disrupts this norm by dissecting complex feature sets into smaller units. Each subsequent unit is converted to individual standalone tests.</p><p>The process of breaking down extensive tests into concentrated units offers an additional advantage: it compels us to thoughtfully consider each functional component of an application before isolating it. This exercise requires a delicate balance — achieving the minimal scope necessary while ensuring that no critical aspect, such as testing key dependencies, is missed. This is in line with my earlier description of Atomicity as the <em>smallest sensible </em>unit of a test designed to verify a specific piece of functionality.</p><p>The definition of what constitutes the ‘smallest sensible unit’ could be debatable. Nicolay Advolodkin suggests that Atomic tests should ideally have just two, or at most three, assertions. Simplifying your tests to this degree might sound challenging, but even small steps towards this goal could enrich your tests with the core characteristics of atomicity: focused functionality, minimalism, and independence. Adopting these can significantly boost your test suite’s overall effectiveness.</p><h3>Final thoughts</h3><p>Shifting towards atomicity can have a powerful impact on testing. By embracing the principles of focused functionality, minimal setup, and independence, we’ve seen how Atomic UI tests can potentially transform not just individual tests, but entire automation test suites.</p><p>The concept of Atomic testing is not just a theoretical ideal but a powerful practical approach that delivers tangible benefits. Faster execution times, improved accuracy, and simpler maintenance are not just aspirations but achievable realities with Atomic UI tests. Whether it’s a small feature or a complex system, the principles of atomicity are a powerful guide for crafting more efficient, effective, and reliable testing practices.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AHLofSZWw3OlTsUH8SNF_Q.png" /></figure><p><strong>If you enjoyed this article,</strong> please join my substack newsletter — <a href="http://blog.refluent.co/"><strong>refluent</strong></a><strong>.<br></strong>We’ll notify you when the next article is published.</p><p>Refluent shares sharp insights on quality engineering, performance testing, and the future of intelligent automation.</p><p>You can also find me on <a href="https://twitter.com/shyamal_tweets">X</a> and <a href="https://www.linkedin.com/in/shyamalraju/">LinkedIn</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*r-rXAaDo96v_KXZUOCNO3w.png" /></figure><h3>References</h3><p>I found the following resources invaluable for my understanding of atomic testing. If you’d like to learn more about Atomic tests, be sure to check them out:</p><ol><li>An excellent resource with code examples by <a href="https://twitter.com/Nikolay_A00">Nikolay Advolodkin</a>: <a href="https://ultimateqa.com/automated-atomic-tests/">Automated Atomic Tests (Definitive Guide)</a></li><li>This excellent podcast episode by the inimitable <a href="https://x.com/joecolantonio?s=20">Joe Colantonio</a> with Nikolay Advolodkin: <a href="https://testguild.com/atomic-tests/">Why Automated Tests Should Be Atomic (The Atomic Punk)</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=dab8af25acbf" width="1" height="1" alt=""><hr><p><a href="https://medium.com/refluent/micro-tests-macro-impacts-the-benefits-of-switching-to-atomic-testing-dab8af25acbf">Micro tests, macro impacts: the benefits of switching to Atomic testing</a> was originally published in <a href="https://medium.com/refluent">refluent</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>