best practices60 min readUpdated Jan 18, 2026
Se

Framework Best Practices

Learn to architect production-ready test automation frameworks using proven design patterns.

Tool:Selenium WebDriverLevel:advancedDomain:QA Engineering

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.

💡 What You'll Learn:
  • 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
🎯 Framework Goals:
  • 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

Architecture

A well-organized project structure is the foundation of maintainable automation. Here's the industry-standard structure used by top companies:

Project Structure
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
💡 Key Principles:
  • 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
pom.xml
<?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 Pattern

The Singleton Pattern ensures only one WebDriver instance exists per thread. Essential for parallel execution and resource management.

Java - DriverManager.java
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;
    }
}
💡 Key Benefits:
  • 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 Pattern

Page Object Model is the most important design pattern in Selenium automation. Each web page is represented by a class containing elements and methods.

Java - BasePage.java
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")
        );
    }
}
Java - LoginPage.java
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);
    }
}
💡 POM Best Practices:
  • Each page has its own class
  • Use @FindBy for 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 Pattern

The Factory Pattern creates objects without exposing creation logic. Useful for creating different browser instances based on configuration.

Java - BrowserFactory.java
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

Configuration

Externalize configuration for different environments (dev, qa, staging, prod). Never hardcode URLs, credentials, or environment-specific values.

config.properties
# 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/
Java - ConfigReader.java
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");
    }
}
💡 Multiple Environment Support:
Java - Environment Enum
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

Utility

Centralize all wait operations to ensure consistency and reduce code duplication.

Java - WaitHelper.java
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

Utility
Java - ScreenshotUtil.java
package 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

Utility
Java - ExcelReader.java
package 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());
        }
    }
}
💡 Usage Example:
Java - DataProvider
@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 Foundation

The BaseTest class contains common setup and teardown logic that all test classes inherit.

Java - BaseTest.java
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();
    }
}
💡 Test Class Example:
Java - LoginTest.java
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

Reporting

Extent Reports provides beautiful, detailed HTML reports with screenshots, logs, and test metrics.

Java - ExtentReportManager.java
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();
    }
}
Java - ExtentReportListener.java
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
log4j2.xml
<?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
testng.xml
<?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>
💡 Multiple Suite Configuration:
smoke-suite.xml
<?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

TestNG
Java - RetryAnalyzer.java
package 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/CD

Integrate your Selenium framework with Jenkins for automated test execution on every code commit.

Jenkinsfile
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/CD

Automate test execution with GitHub Actions on push, pull request, or scheduled intervals.

.github/workflows/selenium-tests.yml
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

Containerization
Dockerfile
FROM 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"]
docker-compose.yml
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=true

9. Best Practices & Anti-patterns

Framework Best Practices

Best Practices
Category✅ Do This❌ Don't Do This
WaitsUse Explicit Waits with ExpectedConditionsUse Thread.sleep() or fixed delays
LocatorsUse ID, CSS Selectors, data-testid attributesUse absolute XPath or index-based locators
Page ObjectsKeep page objects focused on one page/componentPut assertions in page objects
Test DataExternalize in Excel, JSON, or properties filesHardcode test data in test methods
ConfigurationUse config files for environment-specific valuesHardcode URLs, credentials, or timeouts
Driver ManagementUse Singleton pattern with ThreadLocalCreate multiple driver instances
Exception HandlingHandle specific exceptions gracefullyUse empty catch blocks or catch all exceptions
LoggingUse Log4j2 with appropriate log levelsUse System.out.println for all logging
ReportingUse Extent Reports or Allure for detailed reportsRely only on console output
Test OrganizationGroup tests by functionality and priorityPut all tests in one class

Common Anti-patterns to Avoid

Anti-patterns
❌ Anti-pattern #1: Record and Playback

Recording test scripts using IDE tools creates brittle, unmaintainable code. Always write code manually following framework patterns.

❌ Anti-pattern #2: Not Using Page Object Model
Bad Example
// ❌ 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());
}
❌ Anti-pattern #3: Using Thread.sleep()
Bad Example
// ❌ 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")));
❌ Anti-pattern #4: Hardcoding Test Data
Bad Example
// ❌ 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
}
❌ Anti-pattern #5: Test Interdependence
Bad Example
// ❌ 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
}
❌ Anti-pattern #6: Assertions in Page Objects
Bad Example
// ❌ 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
💡 SOLID Principles in Test Automation:
  • 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
💡 DRY Principle (Don't Repeat Yourself):
  • Extract common operations into utility methods
  • Use BasePage for common element interactions
  • Create reusable wait methods in WaitHelper
  • Centralize configuration in ConfigReader
💡 Framework Maintenance Tips:
  • 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

✅ Essential Components 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
🎯 Framework Success Metrics:
  • 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?
📚 Recommended Next Steps:
  • 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.

🚀 Take Your Skills Further:
  • 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

Continue Learning