DEV Community

Cover image for When Should You Refactor Automated Tests? 7 Warning Signs
Mykhailo Krasnovskyi
Mykhailo Krasnovskyi

Posted on

When Should You Refactor Automated Tests? 7 Warning Signs

Only refactor your automated tests when they are still testing the correct things, but the code underlying them makes each change slow, fragile, or risky, and only rewrite them when they are testing the wrong things or when the framework can no longer keep up with where your product is heading.

The majority of suites should be refactored, while only a small number need to be rewritten. The expensive error is to confuse the two.

The 7 Warning Signs

  1. One product change breaks dozens of tests
  2. Every test starts with 40 lines of setup
  3. Sleeps and retries hold the suite together
  4. Tests only pass in one order
  5. Nobody can tell what a test checks
  6. UI tests check logic that doesn't need a UI
  7. Only one person can fix a broken test

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

Why This Matters

A designer changes the name of one button, renaming "Checkout" to "Continue to payment," and the following morning, 63 automated tests fail.

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

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

Why Test Code Rots Faster Than Product Code

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

3 habits speed up the decay:

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

1. One Product Change Breaks Dozens of Tests

The issue is that a single item, for example, a locator, URL, or label, is stored in numerous locations.

javascript
// test_checkout_guest.js
await page.click('#btn-checkout-v2');

// test_checkout_promo.js
await page.click('#btn-checkout-v2');

// ...and 61 more files

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

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

The result is a single product change and a single test change.

2. Every Test Starts With 40 Lines of Setup

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

It hurts in 3 ways:

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

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

The result: is that the tests are shorter, the runs take less time, and new engineers are able to read the suite on their first day.

3. Sleeps and Retries Hold the Suite Together

Search your code for sleep, wait(5000), Thread.sleep, and retry annotations. Each one marks a failure someone patched instead of fixing.

The patches cost you:

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

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

The result is that feedback becomes faster, and the occurrence of red builds up trust within the team.

4. Tests Only Pass in One Order

Run the suite in a random order. If it then fails, it is because the tests depend on one another.

Common symptoms:

  • Test B expects a user that Test A created
  • Tests share one account, and one test changes its settings for all
  • The suite passes locally but fails in parallel CI runs

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

The fix: make every test independent. Each one creates its own data, cleans up after itself, and assumes nothing about what ran before.

The result is parallel runs, shorter pipelines, and failures that indicate the correct area.

5. Nobody Can Tell What a Test Checks

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

Warning signs:

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

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

The payoff: a failing test's name tells your team exactly what broke.

6. UI Tests Check Logic That Doesn't Need a UI

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

When most checks run through the UI, you pay the highest price for each one:

  • Longest execution time
  • Most flakiness from rendering and timing
  • Most maintenance whenever the design changes

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

The result is a set of tools that are both faster and cheaper while still protecting critical flows.

7. Only One Person Can Fix a Broken Test

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

It looks like this:

  • New automation engineers need weeks before touching the framework
  • Custom utilities have no documentation
  • Nobody remembers why key framework decisions were made

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

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

The payoff: any engineer can maintain the suite, and it survives team changes.

The Bigger Picture: Test Automation Debt

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

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

Refactor vs. Rewrite: How to Decide

An unnecessary rewrite takes months. The time required to refactor a suite that needed a rewrite is used up at a slower rate.

Clear signals you need a rewrite

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

Still unsure? Run a POC

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

Real numbers settle the debate faster than meetings.

The Mistake Teams Keep Making

The most common mistake is to halt all testing activity to "fix the suite" in a single major effort. This approach almost never succeeds:

  • Feature work continues, so the product changes under the refactor
  • No new tests get written for weeks, leaving gaps in fresh code
  • The project overruns and gets cut halfway

The other mistake is not arranging the work and then hoping that the engineers will deal with it "when they have time" - deadlines always prevail.

If you want to find out more about what should be fixed, rewritten, or deleted, see our test automation debt guide. That document explains how to prioritize the cleanup without stopping new work.

Automated Test Refactoring Best Practices

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

Quick Gut Check

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

Where to Get Help

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

Our automated testing services team is 100% Middle and Senior QA Engineers. They assess your suite, decide with you whether it needs a refactor or a rewrite, and do the work alongside your releases, with no constant supervision from your side.

The result: faster runs, fewer false alarms and a suite that keeps pace with your product.

Want a straight answer on what your suite needs? Request a QA Assessment, and we'll show you where to start.

Top comments (0)