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.
2. 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]")
3. 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.
4. 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));
5. 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.
6. 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);
7. 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
8. 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);
9. 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.
10. 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
11. 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
12. 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)'
13. 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.
14. 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.
15. 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
Generated for Hybrid Test Automation Framework — Enterprise QA Automation Project
Top comments (0)