Introduction
Building a production-grade Selenium framework requires more than just writing test scripts. This comprehensive guide covers architecture, design patterns, best practices, and real-world implementationused by top QA teams worldwide.
- Industry-standard framework architecture and project structure
- Design patterns: Singleton, Factory, Page Object Model
- Configuration management and environment handling
- Utility classes for common operations
- Reporting, logging, and screenshot management
- CI/CD integration and parallel execution
- Common anti-patterns and how to avoid them
- Maintainability: Easy to update when application changes
- Scalability: Support 100s of tests without performance issues
- Reusability: DRY principle - Don't Repeat Yourself
- Readability: Clear, self-documenting code
- Reliability: Consistent results across environments
1. Framework Architecture
Recommended Project Structure
ArchitectureA well-organized project structure is the foundation of maintainable automation. Here's the industry-standard structure used by top companies:
selenium-framework/ │ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ ├── pages/ # Page Object classes │ │ │ │ ├── BasePage.java │ │ │ │ ├── LoginPage.java │ │ │ │ ├── DashboardPage.java │ │ │ │ └── CheckoutPage.java │ │ │ │ │ │ │ ├── utils/ # Utility classes │ │ │ │ ├── DriverManager.java │ │ │ │ ├── ConfigReader.java │ │ │ │ ├── ExcelReader.java │ │ │ │ ├── WaitHelper.java │ │ │ │ ├── ScreenshotUtil.java │ │ │ │ └── DateTimeUtil.java │ │ │ │ │ │ │ ├── config/ # Configuration │ │ │ │ └── ConfigManager.java │ │ │ │ │ │ │ ├── constants/ # Constants │ │ │ │ ├── FrameworkConstants.java │ │ │ │ └── BrowserType.java │ │ │ │ │ │ │ ├── enums/ # Enumerations │ │ │ │ ├── Environment.java │ │ │ │ └── WaitStrategy.java │ │ │ │ │ │ │ ├── listeners/ # TestNG listeners │ │ │ │ ├── TestListener.java │ │ │ │ ├── RetryAnalyzer.java │ │ │ │ └── ExtentReportListener.java │ │ │ │ │ │ │ └── exceptions/ # Custom exceptions │ │ │ ├── FrameworkException.java │ │ │ └── InvalidBrowserException.java │ │ │ │ │ └── resources/ │ │ ├── config/ │ │ │ ├── config.properties │ │ │ ├── dev-config.properties │ │ │ ├── qa-config.properties │ │ │ └── prod-config.properties │ │ │ │ │ ├── testdata/ │ │ │ ├── testdata.xlsx │ │ │ ├── login-data.json │ │ │ └── users.csv │ │ │ │ │ ├── log4j2.xml # Logging config │ │ └── extent-config.xml # Report config │ │ │ └── test/ │ ├── java/ │ │ ├── base/ # Base test class │ │ │ └── BaseTest.java │ │ │ │ │ ├── tests/ # Test classes │ │ │ ├── LoginTests.java │ │ │ ├── CheckoutTests.java │ │ │ └── SearchTests.java │ │ │ │ │ └── suites/ # TestNG XMLs │ │ ├── smoke-suite.xml │ │ ├── regression-suite.xml │ │ └── testng.xml │ │ │ └── resources/ │ ├── test-output/ # TestNG reports ├── screenshots/ # Failure screenshots ├── logs/ # Application logs ├── extent-reports/ # Extent reports ├── allure-results/ # Allure results │ ├── pom.xml # Maven dependencies ├── .gitignore └── README.md
- Separation of Concerns: Pages, tests, utilities are separate
- Single Responsibility: Each class has one job
- Package Organization: Related classes grouped together
- Test Data Externalization: Data files separate from code
- Configuration Flexibility: Environment-specific configs
Maven Dependencies (pom.xml)
Setup<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.automation</groupId>
<artifactId>selenium-framework</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<selenium.version>4.17.0</selenium.version>
<testng.version>7.9.0</testng.version>
</properties>
<dependencies>
<!-- Selenium Java -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>${selenium.version}</version>
</dependency>
<!-- WebDriverManager (Auto-manage drivers) -->
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.6.3</version>
</dependency>
<!-- TestNG -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
</dependency>
<!-- Extent Reports -->
<dependency>
<groupId>com.aventstack</groupId>
<artifactId>extentreports</artifactId>
<version>5.1.1</version>
</dependency>
<!-- Apache POI (Excel handling) -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<!-- Apache Commons IO (File operations) -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.1</version>
</dependency>
<!-- Log4j2 (Logging) -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.22.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.22.1</version>
</dependency>
<!-- Jackson (JSON handling) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>
<!-- AssertJ (Fluent assertions) -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Surefire Plugin (Run tests) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.3</version>
<configuration>
<suiteXmlFiles>
<suiteXmlFile>src/test/java/suites/testng.xml</suiteXmlFile>
</suiteXmlFiles>
</configuration>
</plugin>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>
</project>2. Essential Design Patterns
Singleton Pattern - DriverManager
Design PatternThe Singleton Pattern ensures only one WebDriver instance exists per thread. Essential for parallel execution and resource management.
package utils;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.edge.EdgeDriver;
import io.github.bonigarcia.wdm.WebDriverManager;
import java.time.Duration;
public class DriverManager {
// ThreadLocal for parallel execution
private static ThreadLocal<WebDriver> driver = new ThreadLocal<>();
private DriverManager() {
// Private constructor to prevent instantiation
}
/**
* Get WebDriver instance for current thread
* @return WebDriver instance
*/
public static WebDriver getDriver() {
return driver.get();
}
/**
* Initialize WebDriver based on browser type
* @param browser Browser name (chrome, firefox, edge)
*/
public static void initDriver(String browser) {
WebDriver webDriver = null;
switch(browser.toLowerCase()) {
case "chrome":
WebDriverManager.chromedriver().setup();
ChromeOptions chromeOptions = getChromeOptions();
webDriver = new ChromeDriver(chromeOptions);
break;
case "firefox":
WebDriverManager.firefoxdriver().setup();
FirefoxOptions firefoxOptions = getFirefoxOptions();
webDriver = new FirefoxDriver(firefoxOptions);
break;
case "edge":
WebDriverManager.edgedriver().setup();
webDriver = new EdgeDriver();
break;
case "headless":
WebDriverManager.chromedriver().setup();
ChromeOptions headlessOptions = getHeadlessChromeOptions();
webDriver = new ChromeDriver(headlessOptions);
break;
default:
throw new IllegalArgumentException("Browser not supported: " + browser);
}
// Configure WebDriver
webDriver.manage().window().maximize();
webDriver.manage().deleteAllCookies();
webDriver.manage().timeouts().implicitlyWait(
Duration.ofSeconds(ConfigReader.getImplicitWait())
);
webDriver.manage().timeouts().pageLoadTimeout(
Duration.ofSeconds(ConfigReader.getPageLoadTimeout())
);
driver.set(webDriver);
}
/**
* Quit WebDriver and remove from ThreadLocal
*/
public static void quitDriver() {
if(driver.get() != null) {
driver.get().quit();
driver.remove();
}
}
/**
* Get Chrome options
*/
private static ChromeOptions getChromeOptions() {
ChromeOptions options = new ChromeOptions();
options.addArguments("--start-maximized");
options.addArguments("--disable-notifications");
options.addArguments("--disable-popup-blocking");
options.addArguments("--disable-blink-features=AutomationControlled");
options.setExperimentalOption("excludeSwitches", new String[]{"enable-automation"});
return options;
}
/**
* Get headless Chrome options
*/
private static ChromeOptions getHeadlessChromeOptions() {
ChromeOptions options = getChromeOptions();
options.addArguments("--headless=new");
options.addArguments("--disable-gpu");
options.addArguments("--window-size=1920,1080");
return options;
}
/**
* Get Firefox options
*/
private static FirefoxOptions getFirefoxOptions() {
FirefoxOptions options = new FirefoxOptions();
options.addArguments("--start-maximized");
return options;
}
}- Thread Safety: ThreadLocal ensures isolation in parallel execution
- Single Responsibility: All driver management in one place
- Centralized Configuration: Browser options managed centrally
- Easy Maintenance: Change driver setup in one location
Page Object Model (POM)
Design PatternPage Object Model is the most important design pattern in Selenium automation. Each web page is represented by a class containing elements and methods.
package pages;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.time.Duration;
public class BasePage {
protected WebDriver driver;
protected WebDriverWait wait;
public BasePage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(15));
PageFactory.initElements(driver, this);
}
/**
* Wait for element to be clickable and click
*/
protected void clickElement(WebElement element) {
wait.until(ExpectedConditions.elementToBeClickable(element));
element.click();
}
/**
* Wait for element and send text
*/
protected void sendText(WebElement element, String text) {
wait.until(ExpectedConditions.visibilityOf(element));
element.clear();
element.sendKeys(text);
}
/**
* Get element text
*/
protected String getElementText(WebElement element) {
wait.until(ExpectedConditions.visibilityOf(element));
return element.getText();
}
/**
* Check if element is displayed
*/
protected boolean isElementDisplayed(WebElement element) {
try {
return element.isDisplayed();
} catch(Exception e) {
return false;
}
}
/**
* Wait for page to load
*/
protected void waitForPageLoad() {
wait.until(driver ->
((JavascriptExecutor) driver)
.executeScript("return document.readyState")
.equals("complete")
);
}
}package pages;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class LoginPage extends BasePage {
// Page Elements
@FindBy(id = "username")
private WebElement usernameField;
@FindBy(id = "password")
private WebElement passwordField;
@FindBy(xpath = "//button[@type='submit']")
private WebElement loginButton;
@FindBy(id = "error-message")
private WebElement errorMessage;
@FindBy(linkText = "Forgot Password?")
private WebElement forgotPasswordLink;
// Constructor
public LoginPage(WebDriver driver) {
super(driver);
}
// Page Methods
public LoginPage enterUsername(String username) {
sendText(usernameField, username);
return this;
}
public LoginPage enterPassword(String password) {
sendText(passwordField, password);
return this;
}
public DashboardPage clickLoginButton() {
clickElement(loginButton);
return new DashboardPage(driver);
}
// Combined method (Method Chaining)
public DashboardPage login(String username, String password) {
enterUsername(username)
.enterPassword(password)
.clickLoginButton();
return new DashboardPage(driver);
}
public boolean isErrorMessageDisplayed() {
return isElementDisplayed(errorMessage);
}
public String getErrorMessage() {
return getElementText(errorMessage);
}
public ForgotPasswordPage clickForgotPassword() {
clickElement(forgotPasswordLink);
return new ForgotPasswordPage(driver);
}
// Verification methods
public boolean isLoginPageDisplayed() {
return isElementDisplayed(loginButton) &&
isElementDisplayed(usernameField);
}
}- Each page has its own class
- Use
@FindByfor element location - Methods should perform actions (click, type) or return information (getText)
- Return next page object after navigation
- Use method chaining for fluent interface
- Keep assertions in test classes, not page classes
Factory Pattern - Browser Factory
Design PatternThe Factory Pattern creates objects without exposing creation logic. Useful for creating different browser instances based on configuration.
package utils;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.safari.SafariDriver;
import io.github.bonigarcia.wdm.WebDriverManager;
public class BrowserFactory {
/**
* Create WebDriver instance based on browser type
*/
public static WebDriver createDriver(String browser) {
WebDriver driver;
switch(browser.toLowerCase()) {
case "chrome":
driver = createChromeDriver();
break;
case "firefox":
driver = createFirefoxDriver();
break;
case "edge":
driver = createEdgeDriver();
break;
case "safari":
driver = createSafariDriver();
break;
case "headless-chrome":
driver = createHeadlessChromeDriver();
break;
default:
throw new IllegalArgumentException("Unsupported browser: " + browser);
}
return driver;
}
private static WebDriver createChromeDriver() {
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
options.addArguments("--start-maximized");
options.addArguments("--disable-notifications");
return new ChromeDriver(options);
}
private static WebDriver createFirefoxDriver() {
WebDriverManager.firefoxdriver().setup();
FirefoxOptions options = new FirefoxOptions();
options.addArguments("--start-maximized");
return new FirefoxDriver(options);
}
private static WebDriver createEdgeDriver() {
WebDriverManager.edgedriver().setup();
return new EdgeDriver();
}
private static WebDriver createSafariDriver() {
return new SafariDriver();
}
private static WebDriver createHeadlessChromeDriver() {
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
options.addArguments("--disable-gpu");
options.addArguments("--window-size=1920,1080");
return new ChromeDriver(options);
}
}3. Configuration Management
Environment Configuration
ConfigurationExternalize configuration for different environments (dev, qa, staging, prod). Never hardcode URLs, credentials, or environment-specific values.
# Browser Configuration browser=chrome headless=false browserVersion=latest # Application URLs baseUrl=https://www.example.com apiUrl=https://api.example.com # Timeouts (in seconds) implicitWait=10 explicitWait=15 pageLoadTimeout=30 scriptTimeout=30 # Test Data defaultUsername=testuser@example.com defaultPassword=Test@123 # Screenshot & Reporting captureScreenshotOnFailure=true screenshotPath=./screenshots/ reportPath=./extent-reports/ # Retry Configuration retryFailedTests=true maxRetryCount=2 # Parallel Execution parallelExecution=false threadCount=3 # Logging logLevel=INFO logPath=./logs/
package config;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
public class ConfigReader {
private static Properties properties;
private static final String CONFIG_FILE_PATH = "src/main/resources/config/config.properties";
static {
loadProperties();
}
private static void loadProperties() {
try {
FileInputStream fis = new FileInputStream(CONFIG_FILE_PATH);
properties = new Properties();
properties.load(fis);
fis.close();
} catch (IOException e) {
throw new RuntimeException("Failed to load config file: " + CONFIG_FILE_PATH, e);
}
}
public static String getProperty(String key) {
String value = properties.getProperty(key);
if(value == null) {
throw new RuntimeException("Property not found: " + key);
}
return value;
}
// Specific getters
public static String getBrowser() {
return getProperty("browser");
}
public static String getBaseUrl() {
return getProperty("baseUrl");
}
public static String getApiUrl() {
return getProperty("apiUrl");
}
public static int getImplicitWait() {
return Integer.parseInt(getProperty("implicitWait"));
}
public static int getExplicitWait() {
return Integer.parseInt(getProperty("explicitWait"));
}
public static int getPageLoadTimeout() {
return Integer.parseInt(getProperty("pageLoadTimeout"));
}
public static boolean isHeadless() {
return Boolean.parseBoolean(getProperty("headless"));
}
public static boolean captureScreenshotOnFailure() {
return Boolean.parseBoolean(getProperty("captureScreenshotOnFailure"));
}
public static String getScreenshotPath() {
return getProperty("screenshotPath");
}
public static String getDefaultUsername() {
return getProperty("defaultUsername");
}
public static String getDefaultPassword() {
return getProperty("defaultPassword");
}
}package enums;
public enum Environment {
DEV("src/main/resources/config/dev-config.properties"),
QA("src/main/resources/config/qa-config.properties"),
STAGING("src/main/resources/config/staging-config.properties"),
PROD("src/main/resources/config/prod-config.properties");
private final String configPath;
Environment(String configPath) {
this.configPath = configPath;
}
public String getConfigPath() {
return configPath;
}
}
// Usage:
// String env = System.getProperty("env", "QA");
// Environment environment = Environment.valueOf(env.toUpperCase());
// ConfigReader.loadConfig(environment.getConfigPath());4. Essential Utility Classes
WaitHelper - Centralized Wait Management
UtilityCentralize all wait operations to ensure consistency and reduce code duplication.
package utils;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.JavascriptExecutor;
import java.time.Duration;
public class WaitHelper {
private WebDriver driver;
private WebDriverWait wait;
public WaitHelper(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(
ConfigReader.getExplicitWait()
));
}
/**
* Wait for element to be visible
*/
public WebElement waitForElementVisible(By locator) {
return wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
}
/**
* Wait for element to be clickable
*/
public WebElement waitForElementClickable(By locator) {
return wait.until(ExpectedConditions.elementToBeClickable(locator));
}
/**
* Wait for element to be present
*/
public WebElement waitForElementPresent(By locator) {
return wait.until(ExpectedConditions.presenceOfElementLocated(locator));
}
/**
* Wait for element to be invisible
*/
public boolean waitForElementInvisible(By locator) {
return wait.until(ExpectedConditions.invisibilityOfElementLocated(locator));
}
/**
* Wait for text to be present in element
*/
public boolean waitForTextPresent(By locator, String text) {
return wait.until(ExpectedConditions.textToBePresentInElementLocated(locator, text));
}
/**
* Wait for element to have attribute
*/
public boolean waitForAttribute(By locator, String attribute, String value) {
return wait.until(ExpectedConditions.attributeToBe(locator, attribute, value));
}
/**
* Wait for title to be
*/
public boolean waitForTitle(String title) {
return wait.until(ExpectedConditions.titleIs(title));
}
/**
* Wait for title to contain
*/
public boolean waitForTitleContains(String partialTitle) {
return wait.until(ExpectedConditions.titleContains(partialTitle));
}
/**
* Wait for URL to be
*/
public boolean waitForUrl(String url) {
return wait.until(ExpectedConditions.urlToBe(url));
}
/**
* Wait for URL to contain
*/
public boolean waitForUrlContains(String partialUrl) {
return wait.until(ExpectedConditions.urlContains(partialUrl));
}
/**
* Wait for alert to be present
*/
public void waitForAlert() {
wait.until(ExpectedConditions.alertIsPresent());
}
/**
* Wait for frame and switch to it
*/
public void waitForFrameAndSwitch(By frameLocator) {
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(frameLocator));
}
/**
* Wait for page to load completely
*/
public void waitForPageLoad() {
wait.until(driver ->
((JavascriptExecutor) driver)
.executeScript("return document.readyState")
.equals("complete")
);
}
/**
* Wait for jQuery to complete (if page uses jQuery)
*/
public void waitForJQueryLoad() {
wait.until(driver -> {
JavascriptExecutor js = (JavascriptExecutor) driver;
return (Boolean) js.executeScript("return jQuery.active == 0");
});
}
/**
* Custom wait with lambda
*/
public <T> T waitForCondition(Function<WebDriver, T> condition) {
return wait.until(condition);
}
/**
* Hard wait (use sparingly!)
*/
public void hardWait(int seconds) {
try {
Thread.sleep(seconds * 1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}ScreenshotUtil - Screenshot Management
Utilitypackage utils;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ScreenshotUtil {
/**
* Capture screenshot and save to file
*/
public static String captureScreenshot(WebDriver driver, String testName) {
try {
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String fileName = testName + "_" + timestamp + ".png";
String screenshotPath = ConfigReader.getScreenshotPath() + fileName;
TakesScreenshot ts = (TakesScreenshot) driver;
File source = ts.getScreenshotAs(OutputType.FILE);
File destination = new File(screenshotPath);
// Create directory if doesn't exist
destination.getParentFile().mkdirs();
FileUtils.copyFile(source, destination);
System.out.println("Screenshot captured: " + screenshotPath);
return screenshotPath;
} catch (Exception e) {
System.out.println("Failed to capture screenshot: " + e.getMessage());
return null;
}
}
/**
* Capture screenshot as Base64 (for reports)
*/
public static String captureBase64Screenshot(WebDriver driver) {
try {
TakesScreenshot ts = (TakesScreenshot) driver;
return ts.getScreenshotAs(OutputType.BASE64);
} catch (Exception e) {
System.out.println("Failed to capture Base64 screenshot: " + e.getMessage());
return null;
}
}
/**
* Capture screenshot as byte array
*/
public static byte[] captureScreenshotAsBytes(WebDriver driver) {
try {
TakesScreenshot ts = (TakesScreenshot) driver;
return ts.getScreenshotAs(OutputType.BYTES);
} catch (Exception e) {
System.out.println("Failed to capture screenshot bytes: " + e.getMessage());
return null;
}
}
/**
* Capture full page screenshot (Selenium 4+)
*/
public static String captureFullPageScreenshot(WebDriver driver, String testName) {
try {
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String fileName = testName + "_fullpage_" + timestamp + ".png";
String screenshotPath = ConfigReader.getScreenshotPath() + fileName;
File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
File destination = new File(screenshotPath);
destination.getParentFile().mkdirs();
FileUtils.copyFile(screenshot, destination);
return screenshotPath;
} catch (Exception e) {
System.out.println("Failed to capture full page screenshot: " + e.getMessage());
return null;
}
}
}ExcelReader - Data-Driven Testing
Utilitypackage utils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class ExcelReader {
private Workbook workbook;
private Sheet sheet;
private String filePath;
public ExcelReader(String filePath, String sheetName) {
this.filePath = filePath;
try {
FileInputStream fis = new FileInputStream(filePath);
workbook = new XSSFWorkbook(fis);
sheet = workbook.getSheet(sheetName);
fis.close();
} catch (IOException e) {
throw new RuntimeException("Failed to load Excel file: " + filePath, e);
}
}
/**
* Get total row count
*/
public int getRowCount() {
return sheet.getLastRowNum() + 1;
}
/**
* Get total column count
*/
public int getColumnCount() {
return sheet.getRow(0).getLastCellNum();
}
/**
* Get cell data as String
*/
public String getCellData(int rowNum, int colNum) {
Cell cell = sheet.getRow(rowNum).getCell(colNum);
return getCellValueAsString(cell);
}
/**
* Get cell data by column name
*/
public String getCellData(int rowNum, String columnName) {
int colNum = getColumnIndex(columnName);
return getCellData(rowNum, colNum);
}
/**
* Get column index by name
*/
private int getColumnIndex(String columnName) {
Row headerRow = sheet.getRow(0);
for(int i = 0; i < headerRow.getLastCellNum(); i++) {
if(getCellValueAsString(headerRow.getCell(i)).equals(columnName)) {
return i;
}
}
throw new RuntimeException("Column not found: " + columnName);
}
/**
* Get all test data as 2D Object array
*/
public Object[][] getAllData() {
int rows = getRowCount() - 1; // Exclude header
int cols = getColumnCount();
Object[][] data = new Object[rows][cols];
for(int i = 1; i <= rows; i++) {
for(int j = 0; j < cols; j++) {
data[i-1][j] = getCellData(i, j);
}
}
return data;
}
/**
* Get test data as List of Maps
*/
public List<Map<String, String>> getDataAsMap() {
List<Map<String, String>> dataList = new ArrayList<>();
Row headerRow = sheet.getRow(0);
for(int i = 1; i < getRowCount(); i++) {
Map<String, String> rowData = new HashMap<>();
Row row = sheet.getRow(i);
for(int j = 0; j < headerRow.getLastCellNum(); j++) {
String key = getCellValueAsString(headerRow.getCell(j));
String value = getCellValueAsString(row.getCell(j));
rowData.put(key, value);
}
dataList.add(rowData);
}
return dataList;
}
/**
* Convert cell value to String
*/
private String getCellValueAsString(Cell cell) {
if(cell == null) {
return "";
}
switch(cell.getCellType()) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
if(DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
}
return String.valueOf((long) cell.getNumericCellValue());
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
return cell.getCellFormula();
case BLANK:
return "";
default:
return "";
}
}
/**
* Close workbook
*/
public void close() {
try {
if(workbook != null) {
workbook.close();
}
} catch (IOException e) {
System.out.println("Failed to close workbook: " + e.getMessage());
}
}
}@DataProvider(name = "loginData")
public Object[][] getLoginTestData() {
ExcelReader reader = new ExcelReader(
"src/main/resources/testdata/testdata.xlsx",
"LoginData"
);
Object[][] data = reader.getAllData();
reader.close();
return data;
}
@Test(dataProvider = "loginData")
public void testLogin(String username, String password, String expectedResult) {
LoginPage loginPage = new LoginPage(DriverManager.getDriver());
loginPage.login(username, password);
if(expectedResult.equals("success")) {
Assert.assertTrue(new DashboardPage(DriverManager.getDriver()).isDisplayed());
} else {
Assert.assertTrue(loginPage.isErrorMessageDisplayed());
}
}5. BaseTest Class
BaseTest Implementation
Test FoundationThe BaseTest class contains common setup and teardown logic that all test classes inherit.
package base;
import org.testng.annotations.*;
import org.openqa.selenium.WebDriver;
import utils.DriverManager;
import utils.ConfigReader;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class BaseTest {
protected static final Logger logger = LogManager.getLogger(BaseTest.class);
@BeforeSuite
public void beforeSuite() {
logger.info("==================== TEST SUITE STARTED ====================");
logger.info("Browser: " + ConfigReader.getBrowser());
logger.info("Environment: " + ConfigReader.getBaseUrl());
}
@BeforeClass
public void beforeClass() {
logger.info("Test Class: " + this.getClass().getSimpleName());
}
@BeforeMethod
@Parameters({"browser"})
public void setUp(@Optional String browser) {
logger.info("Setting up test...");
// Use browser from parameter or config file
String browserType = (browser != null) ? browser : ConfigReader.getBrowser();
// Initialize driver
DriverManager.initDriver(browserType);
// Navigate to base URL
getDriver().get(ConfigReader.getBaseUrl());
logger.info("Browser launched: " + browserType);
logger.info("Navigated to: " + ConfigReader.getBaseUrl());
}
@AfterMethod
public void tearDown(ITestResult result) {
logger.info("Tearing down test...");
// Capture screenshot on failure
if(result.getStatus() == ITestResult.FAILURE) {
logger.error("Test Failed: " + result.getName());
if(ConfigReader.captureScreenshotOnFailure()) {
String screenshotPath = ScreenshotUtil.captureScreenshot(
getDriver(),
result.getName()
);
logger.info("Screenshot saved: " + screenshotPath);
}
} else if(result.getStatus() == ITestResult.SUCCESS) {
logger.info("Test Passed: " + result.getName());
} else if(result.getStatus() == ITestResult.SKIP) {
logger.warn("Test Skipped: " + result.getName());
}
// Quit driver
DriverManager.quitDriver();
logger.info("Browser closed");
}
@AfterClass
public void afterClass() {
logger.info("Test Class Completed: " + this.getClass().getSimpleName());
}
@AfterSuite
public void afterSuite() {
logger.info("==================== TEST SUITE COMPLETED ====================");
}
/**
* Get WebDriver instance
*/
protected WebDriver getDriver() {
return DriverManager.getDriver();
}
}package tests;
import base.BaseTest;
import pages.LoginPage;
import pages.DashboardPage;
import org.testng.annotations.Test;
import org.testng.Assert;
public class LoginTest extends BaseTest {
@Test(priority = 1, description = "Verify successful login with valid credentials")
public void testValidLogin() {
logger.info("Starting test: testValidLogin");
LoginPage loginPage = new LoginPage(getDriver());
DashboardPage dashboard = loginPage.login(
ConfigReader.getDefaultUsername(),
ConfigReader.getDefaultPassword()
);
Assert.assertTrue(dashboard.isDisplayed(), "Dashboard should be displayed");
logger.info("Login successful - Dashboard displayed");
}
@Test(priority = 2, description = "Verify error message with invalid credentials")
public void testInvalidLogin() {
logger.info("Starting test: testInvalidLogin");
LoginPage loginPage = new LoginPage(getDriver());
loginPage.login("invalid@example.com", "wrongpassword");
Assert.assertTrue(loginPage.isErrorMessageDisplayed(), "Error message should be displayed");
Assert.assertEquals(
loginPage.getErrorMessage(),
"Invalid credentials",
"Error message text mismatch"
);
logger.info("Invalid login handled correctly");
}
@Test(priority = 3, description = "Verify login with empty username")
public void testEmptyUsername() {
logger.info("Starting test: testEmptyUsername");
LoginPage loginPage = new LoginPage(getDriver());
loginPage.enterPassword("password123");
loginPage.clickLoginButton();
Assert.assertTrue(loginPage.isErrorMessageDisplayed());
logger.info("Empty username validation working");
}
}6. Reporting & Logging
Extent Reports Integration
ReportingExtent Reports provides beautiful, detailed HTML reports with screenshots, logs, and test metrics.
package listeners;
import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.reporter.ExtentSparkReporter;
import com.aventstack.extentreports.reporter.configuration.Theme;
import utils.ConfigReader;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ExtentReportManager {
private static ExtentReports extent;
private static ThreadLocal<ExtentTest> test = new ThreadLocal<>();
/**
* Initialize Extent Reports
*/
public static void initReports() {
if(extent == null) {
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String reportPath = ConfigReader.getProperty("reportPath") + "ExtentReport_" + timestamp + ".html";
ExtentSparkReporter sparkReporter = new ExtentSparkReporter(reportPath);
// Configure report
sparkReporter.config().setDocumentTitle("Automation Test Report");
sparkReporter.config().setReportName("Test Execution Report");
sparkReporter.config().setTheme(Theme.DARK);
sparkReporter.config().setTimeStampFormat("MMM dd, yyyy HH:mm:ss");
extent = new ExtentReports();
extent.attachReporter(sparkReporter);
// System information
extent.setSystemInfo("Application", "Web Application");
extent.setSystemInfo("Environment", ConfigReader.getBaseUrl());
extent.setSystemInfo("Browser", ConfigReader.getBrowser());
extent.setSystemInfo("OS", System.getProperty("os.name"));
extent.setSystemInfo("User", System.getProperty("user.name"));
}
}
/**
* Create test entry
*/
public static void createTest(String testName, String description) {
ExtentTest extentTest = extent.createTest(testName, description);
test.set(extentTest);
}
/**
* Get current test
*/
public static ExtentTest getTest() {
return test.get();
}
/**
* Flush reports
*/
public static void flushReports() {
if(extent != null) {
extent.flush();
}
}
/**
* Remove test from ThreadLocal
*/
public static void removeTest() {
test.remove();
}
}package listeners;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;
import com.aventstack.extentreports.Status;
import com.aventstack.extentreports.MediaEntityBuilder;
import utils.DriverManager;
import utils.ScreenshotUtil;
public class ExtentReportListener implements ITestListener {
@Override
public void onStart(ITestContext context) {
ExtentReportManager.initReports();
}
@Override
public void onTestStart(ITestResult result) {
ExtentReportManager.createTest(
result.getMethod().getMethodName(),
result.getMethod().getDescription()
);
}
@Override
public void onTestSuccess(ITestResult result) {
ExtentReportManager.getTest().log(Status.PASS, "Test Passed");
}
@Override
public void onTestFailure(ITestResult result) {
ExtentReportManager.getTest().log(Status.FAIL, "Test Failed");
ExtentReportManager.getTest().fail(result.getThrowable());
// Attach screenshot
try {
String base64Screenshot = ScreenshotUtil.captureBase64Screenshot(
DriverManager.getDriver()
);
ExtentReportManager.getTest().fail("Screenshot",
MediaEntityBuilder.createScreenCaptureFromBase64String(base64Screenshot).build()
);
} catch (Exception e) {
ExtentReportManager.getTest().fail("Failed to attach screenshot: " + e.getMessage());
}
}
@Override
public void onTestSkipped(ITestResult result) {
ExtentReportManager.getTest().log(Status.SKIP, "Test Skipped");
ExtentReportManager.getTest().skip(result.getThrowable());
}
@Override
public void onFinish(ITestContext context) {
ExtentReportManager.flushReports();
}
}Log4j2 Configuration
Logging<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Properties>
<Property name="basePath">./logs</Property>
</Properties>
<Appenders>
<!-- Console Appender -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
<!-- File Appender -->
<RollingFile name="File" fileName="${basePath}/automation.log"
filePattern="${basePath}/automation-%d{yyyy-MM-dd}.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<DefaultRolloverStrategy max="10"/>
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>7. TestNG Configuration
Production-Ready testng.xml
TestNG<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Automation Test Suite" parallel="tests" thread-count="3" verbose="1">
<!-- Listeners -->
<listeners>
<listener class-name="listeners.TestListener"/>
<listener class-name="listeners.ExtentReportListener"/>
</listeners>
<!-- Parameters -->
<parameter name="browser" value="chrome"/>
<!-- Smoke Tests -->
<test name="Smoke Tests" preserve-order="true">
<groups>
<run>
<include name="smoke"/>
</run>
</groups>
<classes>
<class name="tests.LoginTest"/>
<class name="tests.HomePageTest"/>
</classes>
</test>
<!-- Regression Tests -->
<test name="Regression Tests">
<groups>
<run>
<include name="regression"/>
</run>
</groups>
<packages>
<package name="tests.*"/>
</packages>
</test>
</suite><?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Smoke Test Suite" parallel="methods" thread-count="5">
<listeners>
<listener class-name="listeners.ExtentReportListener"/>
</listeners>
<test name="Critical Path Tests">
<classes>
<class name="tests.LoginTest">
<methods>
<include name="testValidLogin"/>
</methods>
</class>
<class name="tests.CheckoutTest">
<methods>
<include name="testCheckoutFlow"/>
</methods>
</class>
</classes>
</test>
</suite>Retry Analyzer for Flaky Tests
TestNGpackage listeners;
import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;
import utils.ConfigReader;
public class RetryAnalyzer implements IRetryAnalyzer {
private int retryCount = 0;
private int maxRetryCount = ConfigReader.getProperty("maxRetryCount") != null
? Integer.parseInt(ConfigReader.getProperty("maxRetryCount"))
: 2;
@Override
public boolean retry(ITestResult result) {
if(retryCount < maxRetryCount) {
retryCount++;
System.out.println("Retrying test: " + result.getName() +
" (Attempt " + (retryCount + 1) + ")");
return true;
}
return false;
}
}
// Usage in test:
// @Test(retryAnalyzer = RetryAnalyzer.class)
// public void testMethod() { ... }8. CI/CD Integration
Jenkins Pipeline Configuration
CI/CDIntegrate your Selenium framework with Jenkins for automated test execution on every code commit.
pipeline {
agent any
parameters {
choice(name: 'BROWSER', choices: ['chrome', 'firefox', 'edge'], description: 'Select browser')
choice(name: 'SUITE', choices: ['smoke', 'regression', 'all'], description: 'Select test suite')
choice(name: 'ENVIRONMENT', choices: ['qa', 'staging', 'prod'], description: 'Select environment')
}
tools {
maven 'Maven 3.8.6'
jdk 'JDK 11'
}
stages {
stage('Checkout') {
steps {
echo 'Checking out code from repository...'
checkout scm
}
}
stage('Build') {
steps {
echo 'Building project...'
sh 'mvn clean compile'
}
}
stage('Run Tests') {
steps {
echo "Running ${params.SUITE} tests on ${params.BROWSER} in ${params.ENVIRONMENT}"
script {
def suiteFile = params.SUITE == 'all' ? 'testng.xml' : "${params.SUITE}-suite.xml"
sh """
mvn test \
-Dbrowser=${params.BROWSER} \
-Denvironment=${params.ENVIRONMENT} \
-DsuiteXmlFile=src/test/java/suites/${suiteFile}
"""
}
}
}
stage('Generate Reports') {
steps {
echo 'Generating test reports...'
// Publish TestNG reports
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'test-output',
reportFiles: 'index.html',
reportName: 'TestNG Report'
])
// Publish Extent reports
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'extent-reports',
reportFiles: 'ExtentReport_*.html',
reportName: 'Extent Report'
])
}
}
stage('Archive Artifacts') {
steps {
echo 'Archiving test artifacts...'
archiveArtifacts artifacts: 'screenshots/**/*.png', allowEmptyArchive: true
archiveArtifacts artifacts: 'logs/*.log', allowEmptyArchive: true
}
}
}
post {
always {
echo 'Cleaning up workspace...'
cleanWs()
}
success {
echo 'Test execution completed successfully!'
emailext(
subject: "✅ Test Execution PASSED - ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: """
<p>Test execution completed successfully!</p>
<p><strong>Job:</strong> ${env.JOB_NAME}</p>
<p><strong>Build Number:</strong> ${env.BUILD_NUMBER}</p>
<p><strong>Browser:</strong> ${params.BROWSER}</p>
<p><strong>Suite:</strong> ${params.SUITE}</p>
<p><strong>Environment:</strong> ${params.ENVIRONMENT}</p>
<p><a href="${env.BUILD_URL}">View Build</a></p>
""",
to: 'qa-team@example.com',
mimeType: 'text/html'
)
}
failure {
echo 'Test execution failed!'
emailext(
subject: "❌ Test Execution FAILED - ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: """
<p style="color: red;">Test execution failed!</p>
<p><strong>Job:</strong> ${env.JOB_NAME}</p>
<p><strong>Build Number:</strong> ${env.BUILD_NUMBER}</p>
<p><strong>Browser:</strong> ${params.BROWSER}</p>
<p><strong>Suite:</strong> ${params.SUITE}</p>
<p><strong>Environment:</strong> ${params.ENVIRONMENT}</p>
<p><a href="${env.BUILD_URL}">View Build</a></p>
<p><a href="${env.BUILD_URL}artifact/screenshots/">View Screenshots</a></p>
""",
to: 'qa-team@example.com',
mimeType: 'text/html'
)
}
}
}GitHub Actions Workflow
CI/CDAutomate test execution with GitHub Actions on push, pull request, or scheduled intervals.
name: Selenium Test Execution
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
schedule:
- cron: '0 2 * * *' # Run daily at 2 AM UTC
workflow_dispatch:
inputs:
browser:
description: 'Browser to run tests'
required: true
default: 'chrome'
type: choice
options:
- chrome
- firefox
- edge
suite:
description: 'Test suite to run'
required: true
default: 'smoke'
type: choice
options:
- smoke
- regression
- all
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chrome, firefox]
fail-fast: false
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: maven
- name: Install Chrome
if: matrix.browser == 'chrome'
run: |
wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list'
sudo apt-get update
sudo apt-get install google-chrome-stable
- name: Install Firefox
if: matrix.browser == 'firefox'
run: |
sudo apt-get update
sudo apt-get install firefox
- name: Build with Maven
run: mvn clean compile
- name: Run Tests
run: |
mvn test \
-Dbrowser=${{ matrix.browser }} \
-Denvironment=qa \
-DsuiteXmlFile=src/test/java/suites/smoke-suite.xml
- name: Upload Test Reports
if: always()
uses: actions/upload-artifact@v3
with:
name: test-reports-${{ matrix.browser }}
path: |
test-output/
extent-reports/
logs/
- name: Upload Screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: screenshots-${{ matrix.browser }}
path: screenshots/
- name: Publish Test Results
if: always()
uses: EnricoMi/publish-unit-test-result-action@v2
with:
files: '**/test-output/testng-results.xml'
- name: Send Slack Notification
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: |
Test Execution: ${{ job.status }}
Browser: ${{ matrix.browser }}
Workflow: ${{ github.workflow }}
Commit: ${{ github.sha }}
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}Docker Support
ContainerizationFROM maven:3.8.6-openjdk-11-slim
# Install Chrome
RUN apt-get update && apt-get install -y \
wget \
gnupg \
&& wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list \
&& apt-get update \
&& apt-get install -y google-chrome-stable \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Copy project files
COPY pom.xml .
COPY src ./src
# Download dependencies
RUN mvn dependency:resolve
# Run tests (can be overridden)
CMD ["mvn", "test"]version: '3.8'
services:
selenium-tests:
build: .
volumes:
- ./test-output:/app/test-output
- ./extent-reports:/app/extent-reports
- ./screenshots:/app/screenshots
- ./logs:/app/logs
environment:
- BROWSER=chrome
- HEADLESS=true
- ENVIRONMENT=qa
command: mvn test -Dbrowser=chrome -Dheadless=true9. Best Practices & Anti-patterns
Framework Best Practices
Best Practices| Category | ✅ Do This | ❌ Don't Do This |
|---|---|---|
| Waits | Use Explicit Waits with ExpectedConditions | Use Thread.sleep() or fixed delays |
| Locators | Use ID, CSS Selectors, data-testid attributes | Use absolute XPath or index-based locators |
| Page Objects | Keep page objects focused on one page/component | Put assertions in page objects |
| Test Data | Externalize in Excel, JSON, or properties files | Hardcode test data in test methods |
| Configuration | Use config files for environment-specific values | Hardcode URLs, credentials, or timeouts |
| Driver Management | Use Singleton pattern with ThreadLocal | Create multiple driver instances |
| Exception Handling | Handle specific exceptions gracefully | Use empty catch blocks or catch all exceptions |
| Logging | Use Log4j2 with appropriate log levels | Use System.out.println for all logging |
| Reporting | Use Extent Reports or Allure for detailed reports | Rely only on console output |
| Test Organization | Group tests by functionality and priority | Put all tests in one class |
Common Anti-patterns to Avoid
Anti-patternsRecording test scripts using IDE tools creates brittle, unmaintainable code. Always write code manually following framework patterns.
// ❌ BAD: All locators and logic in test
@Test
public void testLogin() {
driver.findElement(By.id("username")).sendKeys("admin");
driver.findElement(By.id("password")).sendKeys("password");
driver.findElement(By.xpath("//button[@type='submit']")).click();
Assert.assertTrue(driver.findElement(By.id("dashboard")).isDisplayed());
}
// ✅ GOOD: Using Page Object Model
@Test
public void testLogin() {
LoginPage loginPage = new LoginPage(driver);
DashboardPage dashboard = loginPage.login("admin", "password");
Assert.assertTrue(dashboard.isDisplayed());
}// ❌ BAD: Fixed wait
driver.findElement(By.id("submit")).click();
Thread.sleep(5000); // Wastes 5 seconds every time
driver.findElement(By.id("success"));
// ✅ GOOD: Explicit wait
driver.findElement(By.id("submit")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("success")));// ❌ BAD: Hardcoded data
@Test
public void testLogin() {
loginPage.login("testuser@example.com", "Password123");
}
// ✅ GOOD: Externalized data
@Test(dataProvider = "loginData")
public void testLogin(String username, String password, String expectedResult) {
loginPage.login(username, password);
// Verify based on expectedResult
}// ❌ BAD: Test 2 depends on Test 1
@Test(priority = 1)
public void testCreateUser() {
// Creates user and leaves browser on profile page
}
@Test(priority = 2, dependsOnMethods = "testCreateUser")
public void testEditUser() {
// Assumes already on profile page from previous test
}
// ✅ GOOD: Independent tests
@Test
public void testCreateUser() {
// Setup, execute, verify, cleanup
}
@Test
public void testEditUser() {
// Own setup (create user if needed), execute, verify, cleanup
}// ❌ BAD: Assertion in page object
public class LoginPage {
public void login(String user, String pass) {
usernameField.sendKeys(user);
passwordField.sendKeys(pass);
loginButton.click();
Assert.assertTrue(errorMessage.isDisplayed()); // ❌ NO!
}
}
// ✅ GOOD: Return state, assert in test
public class LoginPage {
public void login(String user, String pass) {
usernameField.sendKeys(user);
passwordField.sendKeys(pass);
loginButton.click();
}
public boolean isErrorDisplayed() {
return errorMessage.isDisplayed();
}
}
// Test class
@Test
public void testInvalidLogin() {
loginPage.login("invalid", "wrong");
Assert.assertTrue(loginPage.isErrorDisplayed()); // ✅ YES!
}Code Quality Guidelines
Quality- Single Responsibility: Each class has one job (one page, one utility function)
- Open/Closed: Open for extension (inheritance), closed for modification
- Liskov Substitution: Derived classes should be substitutable for base classes
- Interface Segregation: Many specific interfaces over one general interface
- Dependency Inversion: Depend on abstractions, not concrete implementations
- Extract common operations into utility methods
- Use BasePage for common element interactions
- Create reusable wait methods in WaitHelper
- Centralize configuration in ConfigReader
- Version Control: Use Git with meaningful commit messages
- Code Reviews: All changes should be peer-reviewed
- Documentation: Maintain README with setup instructions
- Refactoring: Regularly refactor to improve code quality
- Dependencies: Keep Maven dependencies up to date
- Monitoring: Track test execution trends and flaky tests
10. Framework Implementation Checklist
Project Setup
- ✅ Maven project with proper structure
- ✅ All required dependencies in pom.xml
- ✅ .gitignore configured properly
- ✅ README with setup instructions
Core Components
- ✅ DriverManager with ThreadLocal
- ✅ ConfigReader for externalized configuration
- ✅ BasePage with common methods
- ✅ BaseTest with setup/teardown
- ✅ WaitHelper for centralized waits
Page Objects
- ✅ One page class per web page
- ✅ Using @FindBy annotations
- ✅ Methods return page objects or data
- ✅ No assertions in page objects
Test Organization
- ✅ Tests grouped by functionality
- ✅ Test methods are independent
- ✅ Using TestNG annotations properly
- ✅ Data-driven tests with @DataProvider
Reporting & Logging
- ✅ Extent Reports configured
- ✅ Log4j2 for logging
- ✅ Screenshots on failure
- ✅ TestNG listeners implemented
CI/CD Integration
- ✅ Jenkins/GitHub Actions pipeline
- ✅ Parameterized execution
- ✅ Automated report publishing
- ✅ Email/Slack notifications
- Maintainability: Can new team members understand and modify code?
- Scalability: Can framework handle 100s of tests without issues?
- Reliability: Are test results consistent across runs?
- Speed: Do tests execute in reasonable time?
- Coverage: Does framework cover all critical user journeys?
- Implement visual regression testing (Applitools, Percy)
- Add API testing with REST Assured
- Integrate with test management tools (JIRA, TestRail)
- Implement parallel execution strategies
- Add performance testing capabilities
- Create mobile testing framework with Appium
🎉 You're Ready to Build Production-Grade Selenium Frameworks!
This comprehensive guide covered everything from architecture to CI/CD integration. You now have the knowledge to build, maintain, and scale enterprise-level test automation frameworks.
- Practice implementing each component in your own projects
- Join our SDET Career Track for hands-on mentorship
- Explore advanced topics like parallel execution and cloud testing
- Build a portfolio project showcasing your framework skills