The Complete Selenium Automation Framework Guide 

An automation framework is the structure around your tests: how browsers are started, where locators live, where test data comes from, how tests run in parallel, how results are reported and how everything runs in CI. A good framework makes adding the 500th test as easy as the 5th; a poor one turns every UI change into a day of fixing scripts. This guide covers how to design a Selenium framework end to end — with the key code — and links every framework tutorial on the site.

Why You Need a Framework

  • Maintainability — a UI change is fixed in one page class, not fifty tests.
  • Reusability — login, waits and navigation are written once.
  • Scalability — parallel and cross-browser runs, many environments.
  • Reliability — consistent synchronisation instead of ad-hoc sleeps.
  • Visibility — reports, screenshots and CI results the whole team can use.

Framework Types

Type Idea Trade-off
Linear (record/playback) Straight scripts, no structure Quick to start, difficult to maintain
Modular Split the app into reusable modules/pages The basis of the Page Object Model
Data-driven Same test, many data sets from Excel/JSON/DB Great coverage; needs good data management
Keyword-driven Actions defined as keywords in a sheet, mapped to code Non-coders can write tests; heavy to build and maintain
BDD Gherkin scenarios mapped to step definitions Business-readable; needs real collaboration
Hybrid A combination — usually POM + data-driven, sometimes BDD Common enterprise approach

When you describe your framework as "hybrid", say what it combines. Most Selenium frameworks are POM + data-driven; only mention keyword-driven if you genuinely have a keyword layer. More: Frameworks & Page Object Model.

The 7 Layers

  1. Base and utilities — driver factory, wait helpers, screenshots, config and data readers, logging.
  2. Page objects — one class per page (or component) with locators and actions.
  3. Tests — TestNG classes with test logic and assertions only.
  4. Test data and configuration — properties per environment, JSON/Excel data.
  5. Execution — testng.xml suites, groups, parallel settings, listeners.
  6. Build — Maven dependencies and the Surefire plugin.
  7. Reporting and CI/CD — Extent/Allure reports, failure screenshots, Jenkins pipelines.

Flow:

Advertisement

Jenkins → Maven → TestNG → tests → page objects → driver factory → browser (local or Grid) → reports

Folder Structure

src/main/java/com/company/
├── base/          → DriverFactory, BasePage
├── pages/         → LoginPage, ProductPage, CartPage, CheckoutPage
└── utils/         → ConfigReader, JsonDataReader, ScreenshotUtil

src/test/java/com/company/
├── tests/         → BaseTest, LoginTest, CartTest, CheckoutTest
└── listeners/     → TestListener (screenshots, report logging), RetryAnalyzer

src/test/resources/
├── config/        → qa.properties, staging.properties
├── testdata/      → users.json, products.xlsx
└── testng.xml, smoke.xml

pom.xml · Jenkinsfile · README.md

The Key Code

1. Driver Factory — One Browser Per Thread

public final class DriverFactory {

    private static final ThreadLocal<WebDriver> DRIVER =
            new ThreadLocal<>();

    public static void init(String browser, boolean headless) {

        WebDriver driver = switch (browser) {

            case "firefox" -> new FirefoxDriver();

            default -> {
                ChromeOptions options = new ChromeOptions();

                if (headless) {
                    options.addArguments(
                            "--headless=new",
                            "--window-size=1920,1080"
                    );
                }

                yield new ChromeDriver(options);
            }
        };

        DRIVER.set(driver);
    }

    public static WebDriver get() {
        return DRIVER.get();
    }

    public static void quit() {
        if (DRIVER.get() != null) {
            DRIVER.get().quit();
            DRIVER.remove();
        }
    }
}

2. Configuration Per Environment

public final class ConfigReader {

    private static final Properties PROPS = new Properties();

    static {
        String env = System.getProperty("env", "qa");
        // Example: mvn test -Denv=staging

        try (InputStream in =
                     ConfigReader.class.getResourceAsStream(
                             "/config/" + env + ".properties")) {

            PROPS.load(in);

        } catch (IOException | NullPointerException e) {
            throw new IllegalStateException(
                    "Missing config for env: " + env, e);
        }
    }

    public static String get(String key) {
        return System.getProperty(
                key,
                PROPS.getProperty(key)
        );
    }
}

System properties override the file, so CI can pass -Dbrowser=firefox without editing anything. Secrets come from environment variables or Jenkins credentials, never from the repository.

3. BasePage — Waits in One Place

public abstract class BasePage {

    protected final WebDriver driver = DriverFactory.get();

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

    protected void click(By locator) {
        wait.until(
                ExpectedConditions.elementToBeClickable(locator)
        ).click();
    }

    protected void type(By locator, String text) {

        WebElement el = wait.until(
                ExpectedConditions.visibilityOfElementLocated(locator)
        );

        el.clear();
        el.sendKeys(text);
    }

    protected String text(By locator) {
        return wait.until(
                ExpectedConditions.visibilityOfElementLocated(locator)
        ).getText();
    }
}

4. A Page Object and a Test

public class LoginPage extends BasePage {

    private final By username = By.id("user-name");
    private final By password = By.id("password");
    private final By loginBtn = By.id("login-button");
    private final By error =
            By.cssSelector("[data-test='error']");

    public ProductsPage loginAs(String user, String pass) {

        type(username, user);
        type(password, pass);
        click(loginBtn);

        return new ProductsPage();
    }

    public String errorMessage() {
        return text(error);
    }
}
public class BaseTest {

    @BeforeMethod
    public void setUp() {

        DriverFactory.init(
                ConfigReader.get("browser"),
                Boolean.parseBoolean(
                        ConfigReader.get("headless")
                )
        );

        DriverFactory.get()
                .get(ConfigReader.get("baseUrl"));
    }

    @AfterMethod(alwaysRun = true)
    public void tearDown() {
        DriverFactory.quit();
    }
}
public class LoginTest extends BaseTest {

    @Test(groups = "smoke")
    public void validUserSeesProducts() {

        Assert.assertTrue(
                new LoginPage()
                        .loginAs(
                                "standard_user",
                                "secret_sauce"
                        )
                        .isLoaded()
        );
    }

    @Test(
            dataProvider = "invalidLogins",
            groups = "regression"
    )
    public void invalidLoginShowsError(
            String user,
            String pass,
            String expected) {

        LoginPage page = new LoginPage();

        page.loginAs(user, pass);

        Assert.assertTrue(
                page.errorMessage().contains(expected)
        );
    }

    @DataProvider
    public Object[][] invalidLogins() {

        return new Object[][] {
                {
                    "locked_out_user",
                    "secret_sauce",
                    "locked out"
                },
                {
                    "standard_user",
                    "wrong",
                    "do not match"
                }
        };
    }
}

Tests read like specifications; locators and waits never appear in them. Store By locators rather than WebElement references to reduce stale-element problems.

5. Listener — Evidence on Failure

public class TestListener implements ITestListener {

    @Override
    public void onTestFailure(ITestResult result) {

        byte[] png =
                ((TakesScreenshot) DriverFactory.get())
                        .getScreenshotAs(OutputType.BYTES);

        // Attach png to your Extent/Allure report
        // under result.getName()
    }
}

Test Data

Keep data out of code: TestNG data providers for small sets, JSON or Excel files for larger ones, and API calls to create data at runtime so tests don't collide. Generate unique values, such as timestamped emails, for anything that must be new.

More: Data-Driven Testing and Collections in Your Framework.

Execution, Parallel Runs and Reports

  • Suites: smoke.xml and regression.xml, or groups selected with -Dgroups=smoke.
  • Parallel: parallel="classes" or "methods" with a sensible thread-count — safe because of the ThreadLocal driver. See Parallel, Grid & Cross-Browser.
  • Reports: Extent or Allure with steps, screenshots and environment details; TestNG XML for CI trends.
  • Retries: an IRetryAnalyzer for genuinely unstable environments only — log every retry and fix the cause.

CI/CD and Scale

A Jenkinsfile checks out the code, runs:

mvn clean test -Denv=qa -Dheadless=true

It then publishes results and archives screenshots — smoke on every merge, regression nightly.

For consistent browsers and more parallelism, run against a Selenium Grid in Docker:

Design Principles and Anti-Patterns

Do Avoid
Explicit waits in one helper layer Thread.sleep() anywhere
Independent tests with their own data Tests that depend on execution order
Stable locators (IDs, data-testid) Absolute XPath and index-based locators
Assertions in tests Assertions buried in page objects
ThreadLocal driver A static shared WebDriver
Config and secrets outside the code Hard-coded URLs and passwords
API calls for setup, UI for what users do Creating all test data through the UI
Small, reusable components 2,000-line "god" page classes

Framework Readiness Checklist

  • Runs with one command (mvn test) on any machine, and headless in CI.
  • Switches browser and environment through parameters.
  • Runs in parallel without random failures.
  • Produces a report with screenshots for every failure.
  • Has a README explaining how to run it and how to add a test.
  • A new team member can add a test in under an hour.

The Learning Path

Part 1 — Framework Concepts

Part 2 — Design and Data

Part 3 — A Complete Build (E-Commerce)

Part 4 — BDD and API Frameworks

How to Use This Guide

  • Building from scratch: the code on this page → Part 2 → Part 3.
  • Interview "explain your framework": the 7 layers above, then Part 1.
  • BDD or API frameworks: Part 4.

Pair it with The Complete Selenium Guide and The Complete SDET Interview Guide, and practise explaining it in the Framework Interview Simulator.

From Real Projects

Across Apkope, Canolog and Testsigma I've done both manual testing and Selenium automation in Java with TestNG, tracked execution in TestRail and defects in Jira, and worked in Agile teams. The topics on this page connect directly to that day-to-day work. Keep the layers separate: tests describe the flow, page classes hold the details.

Frequently Asked Questions

What is a hybrid framework?

A framework that combines several approaches — most commonly the Page Object Model with data-driven testing, sometimes with BDD or keyword-driven elements.

What are the layers of an automation framework?

Base and utilities, page objects, tests, test data and configuration, execution (TestNG), build (Maven), and reporting and CI/CD.

What is the Page Object Model?

A design pattern where each page or component is a class holding its locators and actions, so tests call page methods and locator changes happen in one place.

How is test data separated from test logic?

Data lives in properties, JSON or Excel files, data providers or the database, and is read at runtime — tests contain only logic and assertions.

How do you make a framework run in parallel safely?

Use one WebDriver per thread with ThreadLocal, independent test data, no order dependencies and thread-safe reporting.

How does the framework run in CI/CD?

Jenkins runs Maven, which runs the TestNG suite with parameters for environment and browser, then publishes reports and screenshots — on every merge and nightly.

Should I use PageFactory?

It works, but many teams prefer plain By locators with explicit waits: simpler and less prone to stale-element issues.