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
- 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.
- 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]")
- 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 + "']");
}
}
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.
- 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));
-
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.
- 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);
- 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
- 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);
- 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.
- 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
-
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
-
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)'
- 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.
- 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.
- 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)