Resource library

QA How-To

How to Build a Selenium Java framework from scratch (2026)

Learn to build a Selenium Java framework from scratch with Maven, JUnit 5, page objects, waits, driver lifecycle, parallel tests, and CI reports reliably.

22 min read | 2,969 words

TL;DR

Use Selenium WebDriver with JUnit 5, focused domain objects, deterministic test data, and explicit lifecycle management. Prove one scenario and its failure evidence locally and in CI, then scale without introducing shared mutable state.

Key Takeaways

  • Start Selenium WebDriver with one complete vertical slice before adding framework layers.
  • Keep JUnit 5 lifecycle, automation code, domain abstractions, and assertions clearly separated.
  • Design test data and cleanup for isolated parallel execution from the beginning.
  • Prefer observable conditions and meaningful contracts over sleeps and broad retries.
  • Publish focused failure evidence from CI while redacting secrets and sensitive data.
  • Add abstractions only when they improve domain readability or remove proven duplication.

Build a Selenium Java framework from scratch means creating a small, reliable test system whose responsibilities are obvious to every contributor. This guide gives you a production-minded path using Selenium WebDriver, Java, and JUnit 5, with runnable examples and decisions that remain sound as the suite grows.

The goal is not the largest framework. The goal is fast feedback, trustworthy failures, safe parallel execution, and code that a new SDET can change without decoding hidden behavior. You will build the foundation, organize automation by intent, add diagnostics, and prepare the suite for continuous integration.

TL;DR

Concern Recommended choice Why
Driver binaries Selenium Manager Built into Selenium and maintained with releases
Synchronization Explicit waits Waits for the actual state
Page elements By locators Fresh lookup and simpler stale recovery
Parallel lifecycle One driver per test/thread Prevents cross-test corruption

Start with one end-to-end vertical slice. Keep lifecycle in JUnit 5, automation in Selenium WebDriver, behavior in focused domain objects, and expectations in tests. Run mvn test locally and make the same command the center of CI.

1. Build a Selenium Java framework from scratch: Set Framework Responsibilities

Selenium controls browsers through the W3C WebDriver protocol. JUnit 5 discovers tests and supplies lifecycle hooks. Page and component objects model the interface, while a driver factory creates browser sessions. This division keeps the framework understandable and lets each library do its own job.

Prove the design with one login path before adding reporting, grids, or data providers. A thin vertical slice reveals whether configuration, session cleanup, waits, screenshots, and CI browser dependencies work together.

The review question for this stage is simple: can another engineer explain where this responsibility lives and why? If the answer depends on tribal knowledge, add a small naming improvement or a focused README note. Do not compensate with a large framework manual that becomes stale. Executable examples and conventional locations are better documentation.

2. Bootstrap Maven and Selenium

Use a supported Java release and pin dependencies in Maven. Selenium Manager is included with Selenium and resolves suitable drivers in normal environments, so a separate driver-manager dependency is usually unnecessary. Enterprise networks may still require a mirror or preconfigured driver path.

Keep browser selection, base URL, and headless mode outside test code. System properties are convenient locally, while CI can supply them as command arguments. Validate values before opening a session.

Keep the feedback loop short. Run the narrowest relevant test while developing, then the complete suite before merging. A framework is successful when failures point directly to an application rule, environment dependency, or automation defect. It is not successful merely because it has many layers.

Installation

mvn archetype:generate -DgroupId=com.example.ui -DartifactId=selenium-tests -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
cd selenium-tests
mvn test

3. Organize Pages, Components, and Tests

Page objects should represent pages or stable views. Component objects represent reusable widgets such as headers, tables, or dialogs. Tests express scenarios and assertions. A support package can hold focused diagnostics, but it should not become a miscellaneous dumping ground.

Organize tests by capability when the product has clear domains. Deep technical folder trees make it hard to find coverage. The folder structure should help a new engineer answer where a checkout test and its page objects belong.

Treat configuration as input with a schema. Identify required values, defaults, allowed choices, and sensitive fields. Print safe effective configuration at run start, but redact secrets. This turns many CI mysteries into immediate, actionable messages.

Recommended project tree

pom.xml
src/test/java/com/example/ui/
  config/TestConfig.java
  driver/DriverFactory.java
  pages/LoginPage.java
  pages/components/Header.java
  tests/LoginTest.java
  support/ScreenshotExtension.java
src/test/resources/
  junit-platform.properties

4. Own WebDriver Lifecycle Correctly

Create a fresh driver for each test unless there is a carefully justified alternative. Always call quit in teardown, including after assertion failures. For parallel execution, store a driver per test instance or thread and never expose a mutable static driver.

A factory should create configured Options objects, apply headless or grid settings, and return the session. It should not navigate, log in, or hide failures. RemoteWebDriver can use the same tests when the factory receives a Grid URL.

Design for parallel execution even if the first suite is serial. Avoid global mutable state, fixed record names, shared downloads, and order dependence. Isolation is cheaper to establish with ten tests than to retrofit after one thousand.

Core configuration

<dependencies>
  <dependency>
    <groupId>org.seleniumhq.selenium</groupId><artifactId>selenium-java</artifactId>
    <version>4.44.0</version><scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId>
    <version>5.13.4</version><scope>test</scope>
  </dependency>
</dependencies>

5. Build Page Objects Around User Intent

Methods should describe user operations and return the next page or component when navigation occurs. Store By values instead of long-lived WebElement fields when the DOM frequently rerenders. This allows each operation to find the current element.

Keep assertions in tests unless a page is checking a stable invariant required to prove that it loaded. Avoid PageFactory magic for new designs when ordinary constructors and explicit locators communicate the model more clearly.

Use failure evidence to guide abstraction. Repeated business setup may deserve a builder or client. Repeated low-level calls do not automatically deserve a wrapper, especially when wrapping removes useful library diagnostics or blocks new APIs.

Runnable design example

public final class LoginPage {
  private final WebDriver driver;
  private final WebDriverWait wait;
  private final By email = By.id("email");
  private final By password = By.id("password");
  private final By submit = By.cssSelector("button[type='submit']");

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

  public LoginPage open(String baseUrl) { driver.get(baseUrl + "/login"); return this; }
  public HomePage loginAs(String user, String secret) {
    wait.until(ExpectedConditions.visibilityOfElementLocated(email)).sendKeys(user);
    driver.findElement(password).sendKeys(secret);
    wait.until(ExpectedConditions.elementToBeClickable(submit)).click();
    return new HomePage(driver);
  }
}

6. Synchronize With Explicit Conditions

Use WebDriverWait with ExpectedConditions for visibility, clickability, URL changes, frame availability, alerts, or custom domain conditions. An implicit wait changes every element lookup and can produce confusing combined delays when mixed with explicit waits.

Never use Thread.sleep as synchronization. A sleep waits the full duration when the application is fast and still fails when it is slower. Wait for the observable state that makes the next action safe.

Code review should check readability at the test call site, deterministic cleanup, assertion quality, and the failure message. These qualities matter more than maximizing reuse. A few repeated lines can be safer than one configurable helper with many boolean switches.

Lifecycle or transformation example

public final class DriverFactory {
  public WebDriver create(String browser) {
    return switch (browser.toLowerCase()) {
      case "firefox" -> new FirefoxDriver(new FirefoxOptions());
      case "chrome" -> new ChromeDriver(new ChromeOptions());
      default -> throw new IllegalArgumentException("Unsupported browser: " + browser);
    };
  }
}

class LoginTest {
  private WebDriver driver;
  @BeforeEach void start() { driver = new DriverFactory().create(System.getProperty("browser", "chrome")); }
  @AfterEach void stop() { if (driver != null) driver.quit(); }
}

7. Make Data and Assertions Deterministic

Create unique test records and avoid depending on suite order. Use API or database setup only through approved boundaries, and reserve UI steps for behavior the test intends to cover. Assertions should verify business outcomes, not merely that a click completed without an exception.

When a stale element occurs, first determine why the DOM changed. Re-locate after the state transition instead of adding broad retry loops. Indiscriminate retries can click twice or hide a real product defect.

Maintain a small smoke subset that proves the deployment is testable, then separate broader regression by capability. Tags and markers describe purpose or constraints, not ownership politics. Delete quarantined tests that no longer protect a meaningful risk.

8. Add Diagnostics, Grid, and CI

Capture screenshots, current URL, page source, browser logs where supported, and exception details on failure. Name artifacts with a sanitized test identifier. CI needs a browser or remote Grid, predictable viewport settings, and report publication even when tests fail.

Scale with Grid only after tests are isolated. A larger node count cannot fix shared accounts, fixed filenames, or static drivers. Run a small critical browser set on pull requests and a broader compatibility matrix on a schedule.

Record decisions that affect every contributor, such as naming, lifecycle scope, supported environments, and artifact retention. Leave local implementation details close to code. This balance keeps onboarding practical without creating a second source of truth.

9. How to build a Selenium Java framework from scratch

A practical implementation sequence keeps risk visible. First, commit the smallest dependency and configuration set. Second, automate one representative happy path through the intended public abstractions. Third, force that test to fail and confirm the report tells you why. Fourth, add one negative or boundary case and prove that data cleanup works. Fifth, run two tests concurrently and look for shared state. Finally, place the exact local command in CI.

Do not postpone diagnostics until the suite is large. A framework without useful failure evidence makes every product defect expensive to investigate. Likewise, do not enable maximum parallelism before isolation is measured. A serial test that accidentally depends on another test may appear healthy for months. The first parallel run will expose the dependency, but fixing the design early is much cheaper.

Use pull request review as part of framework governance. A reviewer should be able to identify the scenario, setup, action, expectation, and cleanup without jumping through many files. Shared abstractions should have one reason to change and should not accept unrelated flags. When a contributor needs a special case, decide whether it represents a real domain concept or a one-off test detail.

This is also the stage to connect related skills. Review Selenium explicit wait guide, Selenium locator best practices, Selenium screenshot examples for deeper treatment of selectors, contracts, execution, and troubleshooting that complement this build.

10. Verify the Framework Before Expanding It

Verification should cover more than a green test. Run from a clean checkout, use a fresh dependency cache when feasible, and confirm the documented command works. Deliberately break a locator, endpoint, assertion, or table value. The resulting message should identify the failed operation and preserve enough context to reproduce it. Then interrupt setup or throw from the test and confirm teardown still releases resources.

Run the suite twice to detect leaked state. Reverse test order or use a random order plugin only as a diagnostic, because tests should not need a particular order. Execute with at least two workers if the runner supports it. Check that filenames, ports, accounts, and generated identifiers remain independent. If a test targets a shared environment, make ownership and cleanup rules explicit.

Finally, inspect the published CI experience as a maintainer would. The job name should show the relevant environment and test slice. A failed job should retain its report. Secrets should not appear in console output or artifacts. The suite should return a nonzero exit code for real failures and should not silently convert skipped setup into success. These checks turn a code sample into an operational framework.

11. Production Readiness Checklist

Use this checklist as a release gate for the framework itself. A checked item means the behavior was demonstrated, not merely configured. Save the command and evidence in the pull request so another engineer can repeat the check.

Installation and configuration

  • A clean checkout installs with the declared Maven workflow and does not depend on packages, plugins, browser drivers, or environment files that exist only on the author's machine. Dependency versions are constrained, the relevant manifest is committed, and the supported runtime version is documented.
  • The framework validates its effective environment before test execution. Required URLs, browser choices, tokens, and paths produce direct messages when absent or invalid. Safe defaults are limited to local non-sensitive settings. No secret is committed, printed, placed in a screenshot, or copied into a report.

Lifecycle and isolation

  • Every test can run by itself, first, last, or beside another test. Setup creates the state the scenario needs, while teardown releases sessions and owned records even after an assertion or setup-adjacent operation fails. Rerunning the suite does not encounter leftovers from the prior run.
  • Lifecycle scope matches mutability. A dependency shared across tests is either immutable or deliberately synchronized. Browser sessions, scenario state, request-specific headers, and mutable domain records are not stored in process-wide global variables. Generated files and identifiers include enough uniqueness to avoid worker collisions.

Test design and review

  • Test names describe a rule and expected outcome, not a sequence of clicks or calls. The arrange, act, and assert phases are visible even when helper methods are used. Assertions prove the business result and include values that explain a mismatch. A test does not pass merely because Selenium WebDriver raised no exception.
  • Abstractions expose domain intent and have cohesive responsibilities. There is no catch-all utility class, hidden retry loop, or base class that every test must inherit only to access unrelated helpers. A contributor can still use the underlying library documentation because wrappers preserve standard concepts.

Reliability and diagnostics

  • Synchronization waits for an observable condition and has a bounded timeout. Fixed sleeps are absent from normal test paths. Negative cases distinguish an expected rejection from an infrastructure failure. Any retry policy is visible in runner configuration and accompanied by evidence that allows the first failure to be studied.
  • A deliberately broken test produces actionable evidence. The report identifies the scenario and failed expectation. Relevant artifacts survive a failed CI command and use collision-free names. Sensitive headers, cookies, credentials, personal data, and confidential payload fields are redacted before artifacts leave the runner.

Continuous integration and ownership

  • The main CI command is the same one documented for local use: mvn -B test. CI installs declared dependencies, supplies only approved secrets, returns the correct exit code, and publishes machine-readable results plus focused diagnostics. Pull requests run a fast risk-based subset, while broader coverage has a clear scheduled or release trigger.
  • Every recurring failure has an owner and classification. The team distinguishes product defects, automation defects, environment incidents, test-data conflicts, and intentional changes. Quarantine is time-bound and visible. Historical pass rate does not excuse a test that no longer protects a meaningful requirement.

Change safety

Before declaring the foundation ready, make one small framework change, such as adding an environment, browser, endpoint version, or new table shape. Observe how many files must change and whether existing tests remain untouched. Cross-cutting edits are sometimes legitimate, but routine extension should follow an obvious path. If adding one scenario requires editing a central switch statement and several base classes, revisit the boundaries now.

Also perform a dependency-update rehearsal. Read release notes for Selenium WebDriver and JUnit 5, update on a branch, run the clean-install path, and inspect warnings as well as failures. The framework should make upgrades boring: direct APIs, small adapters at genuine boundaries, and no reliance on undocumented internals. Record only compatibility decisions that future contributors need.

Production readiness does not mean feature completeness. It means the framework has a trustworthy path for installing, executing, failing, diagnosing, cleaning up, and changing. New capabilities can then be added in response to product risk without weakening those properties.

Interview Questions and Answers

Q: What layers belong in a Selenium WebDriver framework?

I keep the runner responsible for lifecycle and results, Selenium WebDriver responsible for automation, domain clients or page objects responsible for business operations, and tests responsible for expectations. Configuration and data builders are focused supporting layers. I add a layer only when it creates a clear boundary.

Q: How do you prevent flaky tests?

I isolate data and state, wait for observable conditions, use stable selectors or contracts, and collect evidence on failure. I do not use arbitrary sleeps or automatic reruns as the primary fix. Retries can help diagnose nondeterminism, but the flaky test remains a defect.

Q: What belongs in a page object or API client?

It should expose cohesive business operations and hide local interaction mechanics. Tests should still own scenario-specific assertions. I avoid generic wrappers that merely rename library methods because they reduce diagnostic value without adding domain meaning.

Q: How do you support parallel execution?

Every test receives independent lifecycle state and unique data. I remove static mutable objects, fixed filenames, shared accounts, and order assumptions. Then I increase workers gradually while monitoring environment limits and artifact collisions.

Q: How should secrets and environment settings be handled?

I inject them through environment variables or an approved secret store and validate them at startup. Defaults are safe only for non-sensitive local values. Logs and reports redact tokens, passwords, cookies, and sensitive payload fields.

Q: What evidence should CI retain?

I retain the runner report, machine-readable results, and focused failure diagnostics. UI suites need screenshots and browser-level evidence, while API suites need sanitized request and response details. Artifact collection must run even when the test command fails.

Q: When is framework abstraction excessive?

It is excessive when engineers must trace several wrappers to understand one action, or when options and boolean flags replace clear use cases. I prefer composition, small domain APIs, and direct library calls where they remain readable.

Q: How do you introduce a new framework to a team?

I deliver one vertical slice, document the few global decisions, and pair on the first contributions. Pull request checks enforce formatting and test execution. I expand capabilities in response to real suite needs instead of predicting every future requirement.

A strong interview answer explains the decision, the tradeoff, and a concrete failure mode it prevents. Adapt these models to projects you have actually worked on rather than reciting terminology.

Common Mistakes

  • Building wrappers around every Selenium WebDriver call before repeated needs exist. This hides familiar APIs and produces longer stack traces.
  • Sharing mutable lifecycle objects or test data across cases. The suite becomes order-dependent and unsafe under parallel execution.
  • Putting scenario-specific assertions inside generic pages, clients, fixtures, or step definitions. This makes negative testing and reuse difficult.
  • Using sleeps, broad retries, or catch-all exception handling to make failures disappear. These techniques delay feedback and conceal the actual synchronization or contract problem.
  • Logging secrets or personal data in reports. Diagnostic value never removes the need for redaction and controlled artifact retention.
  • Treating a successful local run as complete verification. A clean CI environment often reveals undeclared dependencies, path assumptions, missing binaries, and timezone differences.
  • Growing one utility class into a second framework. Split by cohesive responsibility and keep public APIs small.
  • Keeping obsolete tests because they once found a bug. Every test should protect a current risk and have an owner when it fails.

Conclusion

To build a Selenium Java framework from scratch, begin with clear boundaries and one trustworthy vertical slice. Use JUnit 5 for execution, Selenium WebDriver for automation, focused domain objects for readable operations, and deterministic data plus cleanup for isolation. Add evidence and CI before adding scale.

Your next step is concrete: create the project, implement the first representative scenario, make it fail on purpose, and inspect the evidence. Once that experience is fast and clear, expand capability by capability while protecting the same design rules.

Interview Questions and Answers

What layers belong in a Selenium WebDriver framework?

I keep the runner responsible for lifecycle and results, Selenium WebDriver responsible for automation, domain clients or page objects responsible for business operations, and tests responsible for expectations. Configuration and data builders are focused supporting layers. I add a layer only when it creates a clear boundary.

How do you prevent flaky tests?

I isolate data and state, wait for observable conditions, use stable selectors or contracts, and collect evidence on failure. I do not use arbitrary sleeps or automatic reruns as the primary fix. Retries can help diagnose nondeterminism, but the flaky test remains a defect.

What belongs in a page object or API client?

It should expose cohesive business operations and hide local interaction mechanics. Tests should still own scenario-specific assertions. I avoid generic wrappers that merely rename library methods because they reduce diagnostic value without adding domain meaning.

How do you support parallel execution?

Every test receives independent lifecycle state and unique data. I remove static mutable objects, fixed filenames, shared accounts, and order assumptions. Then I increase workers gradually while monitoring environment limits and artifact collisions.

How should secrets and environment settings be handled?

I inject them through environment variables or an approved secret store and validate them at startup. Defaults are safe only for non-sensitive local values. Logs and reports redact tokens, passwords, cookies, and sensitive payload fields.

What evidence should CI retain?

I retain the runner report, machine-readable results, and focused failure diagnostics. UI suites need screenshots and browser-level evidence, while API suites need sanitized request and response details. Artifact collection must run even when the test command fails.

When is framework abstraction excessive?

It is excessive when engineers must trace several wrappers to understand one action, or when options and boolean flags replace clear use cases. I prefer composition, small domain APIs, and direct library calls where they remain readable.

How do you introduce a new framework to a team?

I deliver one vertical slice, document the few global decisions, and pair on the first contributions. Pull request checks enforce formatting and test execution. I expand capabilities in response to real suite needs instead of predicting every future requirement.

Frequently Asked Questions

How long does it take to build a Selenium Java framework from scratch?

A useful first vertical slice can be built in a focused session, but a production-ready framework evolves with real risks. Prioritize lifecycle, one representative scenario, cleanup, diagnostics, and CI before adding integrations.

Which runner should I use with Selenium WebDriver?

This guide uses JUnit 5 because it provides the lifecycle and reporting model shown here. The best runner is one the team understands and can execute consistently in local and CI environments.

Should every test use a page object or client?

Use an abstraction when it expresses stable domain behavior or removes meaningful duplication. A direct library call is acceptable when it is clearer and local to one scenario.

How many tests should run in parallel?

Start with one worker, prove test and data isolation, then increase gradually within environment capacity. There is no universal worker count because browsers, APIs, accounts, and CI machines have different limits.

Should failed tests be retried automatically?

Retries can collect evidence for nondeterministic failures, especially in CI. A test that passes only on retry is still flaky and should be investigated, owned, and fixed.

What should be stored in source control?

Store test code, safe configuration defaults, dependency manifests, and documentation. Never store credentials, access tokens, private keys, or sensitive production data.

How do I know the framework is maintainable?

A new contributor can locate a scenario, understand its setup and assertion, run it with one documented command, and diagnose a deliberate failure. Changes remain local rather than requiring edits across unrelated layers.

Related Guides