All articles
  • Test automation
  • Selenium
  • Regression testing

A First Selenium and Java Suite That Doesn't Rot in Three Months

Most first automation suites are abandoned within a quarter. The reasons are predictable — brittle locators, sleeps, shared test data — and all of them are avoidable.

Muhammad Asadullah Kissana9 min read

The usual life of a first automation suite: two weeks of enthusiasm, forty tests, a demo that goes well, then a UI change breaks half of them. Nobody has time to fix thirty broken tests during a release, so the suite gets skipped "just this once", and three months later it is a folder in the repository that no one runs.

The tests were not wrong. The suite was built in a way that guaranteed this outcome. Here is what I would do differently, having learned it the expensive way with Selenium WebDriver and Java.

Automate the checks you are tired of running

The instinct is to automate the newest, most interesting feature. That is the worst candidate: it is still changing, so the test breaks weekly.

Automate instead the things that are stable, boring, and run every single release. Login. Search. The core create-read-update-delete path on your main entity. Checkout. The five screens that must never break and that you have manually verified thirty times already.

Stability is the selection criterion, not importance. A test against a screen nobody has changed in a year will still pass in a year, and its value compounds. A test against next week's feature is a maintenance liability you have volunteered for.

And be honest about what should not be automated at all: anything visual, anything you would run once, anything where writing the assertion is harder than looking at it. Exploratory testing finds defects automation never will, and time spent automating a one-off is time taken from that.

Locators decide whether your suite survives

This is the whole ballgame. Nothing else you do matters as much.

XPath copied from browser developer tools is the single largest cause of suite death:

// This breaks when anyone adds a div. It will happen this sprint.
driver.findElement(By.xpath("/html/body/div[3]/div/div[2]/form/div[1]/input"));

The fix is to ask developers for stable test attributes. It is a small request and most developers agree readily, because they would rather add an attribute than debug your flaky test:

driver.findElement(By.cssSelector("[data-testid='login-email']"));

That element can move anywhere in the page, be restyled, or be wrapped in three new containers, and the test still passes. Where you cannot get a test attribute, prefer in order: an id, a form field name, a stable CSS class, then text content — and treat absolute XPath as a defect in your own test.

Getting this agreed with the development team before writing tests is the highest-leverage hour in the whole exercise.

Never write Thread.sleep

A sleep is a guess about how slow the application is. On a fast day it wastes seconds. On a slow day the test fails for no reason, and a suite that fails for no reason gets ignored — which is the actual failure mode.

Use explicit waits that wait for a condition:

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

// Wrong: hope four seconds is enough Thread.sleep(4000);

// Right: wait for the thing you actually need wait.until(ExpectedConditions.elementToBeClickable( By.cssSelector("[data-testid='confirm-booking']"))); ```

Wait for the specific state that must hold before the next step: element clickable, element visible, text present, element gone. "Element gone" matters more than people expect — a loading spinner that has not yet disappeared will happily absorb your click.

If a test needs a sleep to pass reliably, that is information: something is racing, and often the race is in the application rather than the test.

Page objects: the one pattern worth learning early

Put locators and interactions for a screen in a class, and let tests speak in user actions. Not because it is a design pattern, but because when the login form changes you want to edit one file rather than forty.

public class LoginPage {
    private final WebDriver driver;
    private final WebDriverWait wait;

private final By email = By.cssSelector("[data-testid='login-email']"); private final By password = By.cssSelector("[data-testid='login-password']"); private final By submit = By.cssSelector("[data-testid='login-submit']");

public LoginPage(WebDriver driver) { this.driver = driver; this.wait = new WebDriverWait(driver, Duration.ofSeconds(10)); }

public DashboardPage loginAs(String user, String secret) { wait.until(ExpectedConditions.visibilityOfElementLocated(email)); driver.findElement(email).sendKeys(user); driver.findElement(password).sendKeys(secret); driver.findElement(submit).click(); return new DashboardPage(driver); } } ```

The test then reads as intent, which also makes it reviewable by someone who does not write Java:

@Test
public void viewerCannotSeeAdminSettings() {
    DashboardPage dashboard = new LoginPage(driver)
        .loginAs(viewerEmail, viewerPassword);

assertFalse(dashboard.hasSettingsLink()); } ```

Two rules keep page objects useful: no assertions inside them — they model the page, the test decides what is correct — and return the page you land on, so a wrong navigation fails at compile time rather than at a confusing assertion.

Every test creates its own data

The most common source of mysterious failures is tests sharing state. Test A leaves a record behind, test B counts rows and gets the wrong number, and whether the suite passes depends on execution order.

So: each test creates what it needs, with a unique identifier, and cleans up after itself. Where the application allows it, create setup data through the API rather than by driving the interface — it is faster, and a failure in setup is then clearly a setup failure rather than a mysterious test failure.

Never point the suite at a database someone else is using by hand. A shared environment guarantees the suite will eventually fail for reasons unrelated to the code, and every unexplained failure spends some of the team's remaining trust.

Assert on state, not on the toast

A success message means the interface decided to show a success message. It does not mean the operation happened.

Where a test is verifying that something was recorded, assert on the recorded state: reload and check the row is there, or query the API for the created object. This is the same reason the interface is not the truth in manual testing, and it applies with more force in automation, where nobody is watching the screen.

Run it somewhere it is annoying to ignore

A suite that runs when someone remembers is a suite that stops running.

Wire it into continuous integration on every pull request, or at minimum on a nightly schedule against the test environment. The tests must run headless and pass consistently in that environment before anyone will trust them, which is itself a useful forcing function: a test that only passes on your machine was never a working test.

Then hold a hard rule — a failing suite blocks the merge, and a flaky test is either fixed the same day or deleted. Keeping a known-flaky test is how a team learns to ignore red builds, and once that habit exists the suite has no value even when it is right.

What automation is not for

Automation confirms that things that used to work still work. That is enormously valuable and it is not the same as finding defects.

New defects are found by someone thinking about what has not been tried — the boundary condition, the impossible sequence, the combination nobody specified. Automation frees time for that work by taking the repetitive regression pass off your hands. It does not replace it, and a team that believes otherwise ends up with a green suite and defects in production.

Start with the checks you are tired of running. Ten stable, trusted tests running on every pull request are worth more than a hundred that everybody has learned to skip. On an ERP in particular, automating the verification layer beats automating the clicks — and the tools I work with day to day are chosen on that basis.

Work with Asadullah Kissana

Available for QA contracts, consulting, and full-time roles — and for web and mobile builds through aimEncoders.