DEV Community

Shell QA
Shell QA

Posted on

Complete Guide to Building a Scalable Java-Selenium Automation Framework

QA Automation - Best Practices Guide
Framework: Hybrid Test Automation Framework
Stack: Java - Selenium 4 - TestNG - Maven - ExtentReports
Project: Enterprise QA Automation

Table of Contents

  • Project Structure
  • Naming Conventions
  • Page Object Model (UI Pages)
  • Business Components
  • Test Scripts
  • Data Management
  • Waits & Synchronization
  • Assertions & Reporting
  • Parallel Execution
  • Run Manager & Configuration
  • ExtentReports
  • CI/CD & Pipeline
  • Error Handling
  • Code Quality
  • Environment Configuration
    1. Project Structure The framework follows a strict layered architecture. Never mix responsibilities across layers. src/ └── test/ ├── java/ │ ├── allocator/ + Entry point & parallel execution engine │ │ ├── Allocator.java │ │ └── ParallelRunner.java │ ├── uiPages/ + Page Object locators ONLY (no logic) │ ├── businessComponents/ + Reusable page actions & validations │ ├── commonComponents/ + Grouped reusable component flows │ └── testscripts/ + Test scripts (keywords/steps only) └── resources/ ├── Run Manager.xlsm + Test execution control └── GlobalSettings.properties

Rules:

  • uiPages: Only By locators (no logic, no assertions).
  • businessComponents: Only reusable actions; never call driver directly in test scripts.
  • testscripts: Only orchestrate business component calls (keyword-driven).
  • allocator: Do NOT modify unless changing threading/reporting strategy.
    1. Naming Conventions | Layer | Class Naming | Method Naming | |---|---|---| | uiPages | page_name_uipages.java | static final By FieldName | | businessComponents | pagePageName.java | camelCase() verb-first | | testscripts | ScriptName.java | execute() or step methods | Examples: // Good - uiPages locator public static final By btn_Submit = By.xpath("//button[@id='btnSubmit']/span");

// Good - business component method
public void fillFormDetails(String primaryField, String secondaryField) throws InterruptedException { }

// Bad - logic inside uiPages
public void clickSubmit() { driver.findElement(btn_Submit).click(); } // Don't do this in uiPages!

Locator ID Preferences (Priority Order):

  • By.id() — most stable
  • By.name()
  • By.cssSelector()
  • By.xpath() — only when no better option exists; avoid absolute XPaths Preferred: By.id("btnSubmit") By.cssSelector("button[data-id='submit']")

Acceptable:
By.xpath("//button[@id='btnSubmit']/span")

Avoid - fragile absolute XPath:
By.xpath("/html/body/div[2]/form/button[1]")

  1. Page Object Model (UI Pages) Each UI page maps to one class in uiPages. Keep locators static and final. // Good structure package uiPages;

import org.openqa.selenium.By;

public class form_details_page {

// Group locators with comments for readability
// === Header Section ===
public static final By txt_referenceNumber = By.xpath("//input[@id='referenceNumber']");
public static final By txt_submissionDate = By.id("submissionDate");

// === Dynamic Locators - use methods ===
public static By get_recordLink(String recordId) {
    return By.xpath("//a[text()='" + recordId + "']");
}
Enter fullscreen mode Exit fullscreen mode

}

Rules:

  • One class per page/module.
  • Use static final for all fixed locators.
  • Use static methods for dynamic/parameterized locators.
  • Group related locators with inline comments.
  • Do not import Selenium WebDriver or WebElement in UI pages.
    1. Business Components Business components contain the actual Selenium interactions and extend GeneralComponents (which extends ReusableLibrary). // Good business component method public void fillFormDetails(String userName, String effectiveDate) { sendKeys(form_details_page.txt_userName, userName, "User Name"); sendKeys(form_details_page.txt_effectiveDate, effectiveDate, "Effective Date"); report_updateTestLog("Fill Form Details", "User: " + userName + " | Date: " + effectiveDate, Status.PASS); }

// Bad - raw driver calls inside a business component
driver.findElement(By.xpath("//input[@id='userName']")).sendKeys(userName);

Rules:

  • Always use GeneralComponents helper methods (clickElement, sendKeys, selectDropDownByValue, etc.) — never call driver.findElement() directly.
  • Every significant action should update the report via report_updateTestLog.
  • Use wait (FluentWait and explicitWait) defined in GeneralComponents — never use Thread.sleep().
  • Use PauseScript() only as a last resort; prefer explicit waits.
  • Keep methods atomic — one action per method when possible. FluentWait Usage: // Correct - use inherited waits wait.until(ExpectedConditions.visibilityOfElementLocated(signin_home_page.welcomeText));
  1. Test Scripts
    Test scripts are the entry point for each test case. They call business component methods in sequence.
    // Good test script structure
    public class TC_001_CreateNewRecord extends ScriptHelper {

    public void execute() {
    // Step 1: Login
    pdf_login_new = new pdf_login_helper();
    login.login();

    // Step 2: Navigate to New Record
    pdf_home_new = new pdf_home_helper();
    home.clickNewRecord();
    
    // Step 3: Fill form details
    pdf_formDetails = new pdf_formDetails_helper();
    formDetails.fillFormDetails(
        dataTable.getData("TestData", "UserName"),
        dataTable.getData("TestData", "EffectiveDate")
    );
    

    }
    }

Rules:

  • Test scripts must not contain XPaths, locators, or driver calls.
  • Each step should be a business component call.
  • Use dataTable.getData() for test data — never hardcode values.
  • Keep test scripts short and readable (under 100 lines where possible).
  • One test script = one test scenario.
    1. Data Management Excel Run Manager:
  • Control test execution in Run Manager.xlsm — set Execute column to Yes/No.
  • Use TestConfig_optionsID to reference browser/environment config from the TestConfig_options sheet.
  • Set IterationMode to RUN_ALL_ITERATIONS, RUN_ONE_ITERATION_ONLY, or RUN_RANGE_OF_ITERATIONS. Test Data: // Read Data from Excel data sheet String email = dataTable.getData("TestData", "Email"); String country = dataTable.getCommonData("country", "DataValue");

// Never hardcode test data
String email = "user@example.com"; // Don't do this

Data Best Practices:
| Rule | Reason |
|---|---|
| Use CommonData sheet for shared values (email, URL, etc.) | Single source of truth |
| Use scenario-specific sheets for test-specific data | Separation of concerns |
| Do not store passwords in plain text in Excel | Security |
| Use JavaFaker for generating random test data where applicable | Data independence |
// Using JavaFaker for random data
Faker faker = new Faker();
String productName = faker.company().name();
String productRef = faker.number().digits(8);

  1. Waits & Synchronization Wait Hierarchy (use in this order): | Wait Type | When to Use | Config Key | |---|---|---| | wait (FluentWait) | Standard UI elements | wait in properties | | longwait (FluentWait) | Slow-loading pages/modals | longwait in properties | | ExpectedConditions.visibilityOf | Checking visibility before interaction | — | | ExpectedConditions.elementToBeClickable | Before clicking dynamic elements | — | | PauseScript() | Last resort only — unavoidable timing gaps | — | Best practice wait pattern: try { wait.until(ExpectedConditions.visibilityOfElementLocated(locator)); clickElement(locator, "Button Name"); } catch (TimeoutException e) { report_updateTestLog("Step", "Element not visible after timeout", Status.FAIL); }

// For elements that may or may not appear
try {
wait.until(ExpectedConditions.visibilityOfElementLocated(optionalElement));
clickElement(optionalElement, "Optional Button");
} catch (TimeoutException | NoSuchElementException e) {
report_updateTestLog("Step", "Optional element not present - skipping", Status.PASS);
}

// Never use
Thread.sleep(5000);

Configuring Wait Timeouts:
Set in GlobalSettings.properties:
wait=30
longwait=60

  1. Assertions & Reporting Status Levels: | Status | When to Use | |---|---| | Status.PASS | Assertion passed / action successful | | Status.FAIL | Critical assertion failed — stops test | | Status.WARNING | Non-critical mismatch (e.g., dynamic external values) | | Status.DONE | Informational step (no assertion) | Good assertion pattern: if (driver.findElement(welcomeText).isDisplayed()) { report_updateTestLog("Login Validation", "Homepage loaded successfully", Status.PASS); } else { report_updateTestLog("Login Validation", "Homepage did NOT load", Status.FAIL); }

// Use framework helper for value comparison
validateActualExpectedValue(actualValue, expectedValue, "Field Name");

// Use WARNING for values dependent on external systems (APIs, DBs)
validateActualExpectedValueForUnpredictableStringValue(actualValue, expectedValue, "Calculated Field");

Log Message Best Practices:
// Good - descriptive, includes actual value
report_updateTestLog("Field Validation",
"Actual: " + actualValue + " | Expected: " + expectedValue, Status.PASS);

// Bad - too vague
report_updateTestLog("Check", "OK", Status.PASS);

  1. Parallel Execution The framework supports three threading models via Allocator.java: | Method | Description | When to Use | |---|---|---| | executeTestBatch() | Fixed ThreadPoolExecutor | Production stable runs | | executeTestBatch_Virtual() | Fixed ThreadPool (active) | Default - best for most scenarios | | executeTestBatch_AutoThreadPool() | WorkStealingPool | Experimental - perf testing | Thread Safety Rules:
    • Never use static mutable state in business components or test scripts.
    • referenceNumber in GeneralComponents is static — use with caution in parallel runs; prefer passing values as parameters.
    • Each ParallelRunner instance is independent — ensure no shared file/resource access without synchronization. // Thread-safe - instance variable private String referenceNumber;

// Risk in parallel - static shared variable
public static String referenceNumber = null; // Avoid in parallel scenarios

Configuring Threads:
GlobalSettings.properties:
NumberOfThreads=3

  • Recommended: Set threads based on available machine CPUs (e.g., threads = CPUs * 2).
  • Caution: Too many threads on a single machine causes driver conflicts and flaky tests.
    1. Run Manager & Configuration Run Manager Sheet Columns: | Column | Value | Notes | |---|---|---| | Execute | Yes / No | Controls if test runs | | TestScenario | Package/folder name | Maps to test script folder | | TestCase | Class name | Exact Java class name | | IterationMode | RUN_ALL_ITERATIONS | Controls data iteration | | StepsToExecute/EndIteration | Number | For ranged iterations | | TestConfigurationID | Config name | Maps to TestConfigurations sheet | TestConfigurations Sheet: | Column | Example Values | |---|---| | ExecutionMode | LOCAL, REMOTE, GRID | | Browser | EDGE, CHROME, FIREFOX | | Platform | WINDOWS, LINUX | | ExecutionTimeout | 120, leave blank for auto | GlobalSettings.properties Key Properties: # Execution RunConfiguration=Regression # Sheet name in Run Manager NumberOfThreads=2 DefaultBrowser=EDGE DefaultExecutionMode=LOCAL DefaultPlatform=WINDOWS Environment=SIT # SIT or UAT

URLs

uialuat_SIT=https://sit.app.example.com
uialuat_UAT=https://uat.app.example.com

Waits (seconds)

wait=30
longwait=60

Reporting

ProjectName=AutomationProject
GenerateHTMLReport=false

  1. ExtentReports
    Reports are auto-generated in target/Reports/Extent Result/ExtentReport.html.
    Best Practices:

    • Always call extentReportFlush() at the end — already handled in Allocator.driveBatchExecution().
    • Ensure report_updateTestLog() is used — it auto-attaches screenshots on failure.
    • Never create a new ExtentReports instance inside test scripts. Viewing Reports: target/ └── Reports/ ├── Extent Result/ │ └── ExtentReport.html • Open in browser └── HTML Reports/ └── index.html
  2. CI/CD & Pipeline
    Maven Profiles:

    Run via Allocator (Hybrid Framework) - DEFAULT

    mvn clean test -PRunAllocator

Run via TestNG suite

mvn clean test -PRunTestNGTests

Pass run configuration at runtime

mvn clean test -PRunAllocator -DRunConfiguration=SIT_Smoke

Pass environment at runtime

mvn clean test -PRunAllocator -DRunConfiguration=SIT_Smoke -DEnvironment=SIT

Azure Pipeline (azure-pipelines.yml):

  • Ensure NumberOfThreads is set to match pipeline agent specs.
  • Use pipeline variables for environment switching instead of hardcoding.
  • Archive target/Reports/ as a pipeline artifact for post-run analysis. # Good pipeline variable usage
    • task: Maven@3 inputs: goals: 'clean test' options: '-PRunAllocator -DRunConfiguration=$(RunConfig) -DEnvironment=$(Env)'
  1. Error Handling Exception Handling Pattern: // Catch specific exceptions and report try { wait.until(ExpectedConditions.visibilityOfElementLocated(locator)); clickElement(locator, "Submit Button"); } catch (TimeoutException e) { report_updateTestLog("Submit", "Element not visible - TimeoutException", Status.FAIL); } catch (NoSuchElementException e) { report_updateTestLog("Submit", "Element not found in DOM", Status.FAIL); }

// For optional elements (may or may not appear)
try {
clickElement(optionalPopup, "Optional Popup OK");
} catch (NoSuchElementException | TimeoutException e) {
// Silently skip - optional element
}

// Never swallow exceptions silently without a log
try {
// ...
} catch (Exception e) {
// Empty catch - bad practice
}

Stop Execution on Critical Failure:
The ParallelRunner checks frameworkParameters.getStopExecution() — use this mechanism when a prerequisite test fails and subsequent tests cannot proceed.

  1. Code Quality General Rules: | Rule | Detail | |---|---| | DRY | Extract repeated actions into GeneralComponents or CommonMethods. | | Single Responsibility | Each method does one thing. | | Descriptive Names | fillFormDetails() not step1(). | | No Magic Numbers | Use named constants or data table values. | | Comment Why, Not What | Code is self-explanatory; comments explain business logic. | | Remove Dead Code | Don't leave commented-out blocks; use version control instead. | Preferred Patterns: // Extract repeated logic into CommonMethods public String getProcessedReferenceNumber(String refNumber) { return refNumber.substring(0, refNumber.length() - 1) + "B"; }

// Use constants instead of magic strings
private static final String PASS_STATUS = "PASS";
private static final String ENVIRONMENT_SIT = "SIT";

// Null/empty guard
if (testData != null && !testData.isEmpty()) {
getTestControlObject().getReport().reportEvent("TestControlData", testData, Status.PASS);
}

Code Review Checklist:

  • No hardcoded URLs, credentials, or test data.
  • All driver.findElement() calls go through GeneralComponents helpers.
  • All XPaths are relative (not absolute).
  • All exceptions are caught and logged to report.
  • Report steps have meaningful descriptions.
  • No Thread.sleep() in code.
  • Run Manager updated for new test cases.
  • UI page locators added to correct uiPages class.
    1. Environment Configuration Switching Environments: Environment is controlled via GlobalSettings.properties or pipeline parameter: Environment=SIT or UAT Business components should always read URLs from properties: // Good - environment driven if (properties.getProperty("Environment").equals("SIT")) { driver.get(properties.getProperty("uialuat_SIT")); } else if (properties.getProperty("Environment").equals("UAT")) { driver.get(properties.getProperty("uialuat_UAT")); }

// Bad - hardcoded URL
driver.get("https://sit.app.example.com");

Supported Environments:
| Key | Description |
|---|---|
| SIT | System Integration Testing |
| UAT | User Acceptance Testing |
Quick Reference - Do's and Don'ts
| DO | DON'T |
|---|---|
| Use GeneralComponents helper methods | Call driver.findElement() directly in test scripts |
| Use FluentWait with ExpectedConditions | Use Thread.sleep() |
| Read all test data from Excel/properties | Hardcode values in test scripts |
| Report every step with report_updateTestLog() | Leave silent catch blocks |
| Keep locators in uiPages only | Put locators in business components |
| Use relative XPaths | Use absolute XPaths |
| Use Status.WARNING for dynamic external values | Fail tests for known external dependencies |
| Flush ExtentReport at end of run | Create multiple ExtentReport instances |
| Set Execute=No to skip tests | Delete rows from Run Manager |
| Use parameterized locator methods for dynamic elements | Concatenate XPath strings inline |
Support
Automation Team: QE Architect
automation.team@example.com
Generated for Hybrid Test Automation Framework — Enterprise QA Automation Project

Top comments (0)