Guide

Self-healing tests fix the red. They do not tell you what broke.

Self-healing repairs selectors, waits and sometimes asserts. What it fixes, what it quietly hides, and the three cases where healing is the right call.

10 min read

On this page

Self healing test automation is sold as less maintenance, and it delivers: a button gets a new ID, the test finds it anyway, and nobody spends Tuesday fixing selectors. The catch is how: it makes the test agree with whatever the application now does. That is right for a renamed button, and the worst thing a test can do for a checkout that now charges twice.

This post is about telling those two cases apart. Healing is one layer of AI test automation, and worth having. It just answers a different question from the one a failing test asks.

What is self-healing test automation?

Self-healing test automation is a runner that repairs a failing test instead of reporting it. It does three things: re-finds an element whose selector broke, stretches waits when a step times out, and, in some setups, loosens an assertion whose expected value no longer matches. What changed is recorded, if at all, in a log.

That last sentence is the whole argument. Self healing tests keep the signal green. Whether anyone reads why is a process question.

The three mechanisms

Self healing test automation tools all do some mix of three repairs, and the three carry very different risks.

MechanismWhat it repairsWhat it can hide
Selector repairAn element whose ID, class, text or position changed; the runner picks the best-scoring nearby candidateThe wrong element: the first line item instead of the total, or an old button that is no longer wired
Wait and timing adjustmentA step that timed out; the runner waits longer or retriesA real slowdown, or a request that now takes two round trips
Assertion softeningAn expected value that no longer matches; the check widens to a pattern, a range or the new valueA changed number: the test now asserts the bug

Selector repair is the most common need. In a study of 1,065 breakages across eight open-source web apps, Hammoudi, Rothermel and Tonella found that "locators caused over 73% of the test breakages we observed" (ICST 2016, summarised by The Morning Paper). That is the biggest pile, and why automated test healing caught on.

The masking problem in self healing test automation

A selector breaks for one of two reasons: the markup changed and the behaviour did not, or the behaviour changed and the markup changed with it. Selector repair cannot tell which. It finds the closest element and carries on.

One checkout test, two releases.

Release A renames the button. #pay-btn labelled "Pay" becomes #checkout-submit labelled "Place order". Same handler, same request. Healing re-finds the button, the test passes, and that pass is true.

Release B changes the total. A refactor splits shipping into its own charge, so the card is charged twice and the one .order-total of $42.00 becomes two lines, $42.00 and $6.99. The old selector is gone, so the healer picks the closest match, the first line, and reads $42.00. The assertion passes.

Two releases of the same checkout test. Release A renames the Pay button to Place order: healing re-finds it and the green is true. Release B splits the total into two charges: healing picks the first line, reads 42 dollars, and the green hides a second charge.

Both runs are green, and both reports say the same thing: one healed step, logged at info level. Release B ships a double charge with a passing test as its alibi.

Had the setup softened the assertion to "a dollar amount is shown" instead, the outcome is the same: a loud failure became a quiet one. Retries do the same thing to flaky tests, and teach the team the same habit.

Three cases where healing is the right call

None of this makes self healing automation a bad idea. In three situations it is the correct tool, and refusing it just burns time.

Cosmetic refactors. Class names change in a CSS rewrite, generated IDs change between builds, wrappers appear around buttons. Behaviour is identical, and a test that fails on a renamed class is testing the stylesheet. Heal these and move on.

Framework or design-system migrations. Moving a few hundred screens to a new component library touches every selector at once. Without repair, the suite is red for weeks and nobody trusts it by the end. Healing carries it through while the markup churns.

Third-party DOM you do not control. Payment iframes, chat widgets and identity-provider login pages change markup on someone else's schedule. You cannot pin it, and you mostly care that the step completes. Repair the selector, and check your own side of the exchange: the token came back, the session exists.

In all three, the markup moved and the behaviour did not. That is the condition healing assumes, and when it holds, healing is pure saving.

The alternative: re-derive the verdict

The fix for masking is to stop letting the selector decide the verdict. If the assertion reads what the application did (which requests fired, what the store holds), finding the button and judging the outcome become separate jobs. The verdict is computed fresh from recorded evidence, which is the core idea of AI verification: judge the outcome, not the screen.

Deterministic replay works this way. You record the path once, with the expected consequence of each step. Every run replays it, collects network and state again, and re-derives pass or fail. Nothing is repaired: if the evidence differs, the run fails and names the difference.

Before, a check on what the page shows, which a healer can satisfy by picking a different element:

await click("#pay-btn");                       // healed to #checkout-submit
await expectText(".order-total", "$42.00");    // healed to the first line item

After, a check on what the app did, which no element choice can satisfy. The helper names are illustrative; the shape is what matters:

await click(byRole("button", /pay|place order/i));
expect(requests("POST /api/charge")).toHaveLength(1);   // release B: 2
expect(state("order.total")).toBe(4200);                // release B: 4899
expect(state("order.status")).toBe("paid");

Release A passes both. Release B passes the first and fails the second on two lines, each saying what changed. The second test does not care what the button is called, only that one click made one charge for the right amount.

Repair versus re-derivation. Repair takes a failing step, finds a new element, and turns the run green, logging the change. Re-derivation replays the recorded path, reads requests and state, and computes the verdict, reporting the difference when one exists.

The failure modes this catches, from double submits to swallowed errors, are listed in seven failure modes of AI-generated code. This post does not repeat them.

Cost and stability, side by side

Self-healingDeterministic replay
Maintenance effortLow for markup churn; healed steps should still be reviewedLow for markup churn if the step targets role and text; a changed outcome needs a re-recording
Flake rateNot measured here; each run re-searches the page, so a match can vary between runs0% in our benchmark: eight replays of one flow, one verdict
What a failure tells youUsually that no candidate scored high enoughWhich request, state value or console line differed from the recording
Cost per re-runDepends on the tool; model-based matching spends tokens on each healed stepAbout 480 tokens to re-verify a four-flow suite, with no model in the loop

The two figures come from the benchmark below. We have not measured a healing tool's flake rate, so that column states only the mechanism. For what other kinds of tool read to decide pass or fail, see our comparison of regression testing tools.

A decision rule

Ask of each failure: did the behaviour change, or only the markup?

SituationUse healingRe-derive the verdict
Class names and IDs changed in a refactorYesNot needed
Migrating to a new component libraryYes, through the migrationKeep the outcome checks running alongside
A third-party widget changed its markupYes, for the widget's own stepsYes, for your side of the exchange
A price, total or quantity changedNoYes
A request count or endpoint changedNoYes
A test fails and nobody knows whyNo: healing would hide the answerYes: the diff is the answer

The short version: heal the path, never the verdict.

Reticle is built on the right-hand column: it records flows inside the running app, with network, state and console for each step, and replays them with no model in the loop. In its published benchmark, re-verifying a four-flow suite costs about 480 tokens, and eight replays of one flow gave one verdict (0% flake). It does not replace healing for markup churn, and a person still decides the right total.

Fun fact

"Self-healing" did not start in testing. It is one of the four self-management properties of autonomic computing, an initiative IBM launched in 2001 and named after the autonomic nervous system, the part of your body that keeps your heart beating without you noticing (Wikipedia).

The property is summarised there as "automatic discovery, and correction of faults". Discovery comes first. Test healing kept the correction and quietly dropped the part where someone finds out.

Conclusion

Self-healing tests solve a real problem: most breakages are selectors, and most selector changes are cosmetic. But a healed test agrees with the application, which is only safe when the behaviour did not change. Heal the path through markup churn, migrations and third-party widgets. For money, counts and state, re-derive the verdict from what the app did.

FAQ

What is self-healing test automation?

It is a test runner that repairs failing tests instead of reporting them. It re-finds elements whose selectors broke, adjusts waits when steps time out, and sometimes loosens assertions whose expected values changed. The goal is lower maintenance. The cost is that a repaired test agrees with the app, whether or not the app is right.

Is self-healing test automation reliable?

It is reliable at keeping tests running through markup changes, which cause most breakages. It is not reliable at telling you whether the behaviour is still correct, because a repair can match the wrong element or accept a changed value. Review healed steps, and pair healing with assertions on network and application state.

Does self-healing fix flaky tests?

It hides some of them. Timing repair absorbs slow runs, so a timing flake stops failing, but the cause is still there and the test still reads the wrong signal. A flaky test is fixed by asserting on an event or a state the code produces. Healing reduces the red without removing the coin flip.

When should you not use self-healing?

Do not let it repair a step whose outcome matters: a total, a charge, a request count, a permission check or anything stored. Do not use it when a test fails for an unknown reason, because the repair erases the evidence. Keep it for cosmetic refactors, design-system migrations and third-party markup you do not control.