Lesson 3 of 14 ~30 min
Course progress
0%

Framework Design Best Practices

Build scalable, maintainable automation frameworks

Enterprise Framework Architecture

Learn to build production-ready automation frameworks following industry best practices.

Project Structure

selenium-framework/
├── src/
│   ├── main/
│   │   └── java/
│   │       └── com/automation/
│   │           ├── config/
│   │           │   ├── ConfigReader.java
│   │           │   └── DriverFactory.java
│   │           ├── pages/
│   │           │   ├── BasePage.java
│   │           │   ├── LoginPage.java
│   │           │   └── HomePage.java
│   │           ├── utils/
│   │           │   ├── WaitHelper.java
│   │           │   ├── ScreenshotUtil.java
│   │           │   └── ExcelReader.java
│   │           └── constants/
│   │               └── AppConstants.java
│   └── test/
│       ├── java/
│       │   └── com/automation/
│       │       ├── tests/
│       │       │   ├── BaseTest.java
│       │       │   └── LoginTest.java
│       │       └── listeners/
│       │           └── TestListener.java
│       └── resources/
│           ├── config.properties
│           ├── testng.xml
│           └── testdata/
│               └── users.xlsx
├── reports/
│   └── extent-reports/
├── screenshots/
├── logs/
├── pom.xml
└── README.md

Configuration Management

config.properties:

# Application
base.url=https://example.com
app.timeout=10

# Browser
browser=chrome
headless=false
maximize=true

# Grid
grid.enabled=false
grid.url=http://localhost:4444

# Database
db.url=jdbc:mysql://localhost:3306/testdb
db.username=root
db.password=secret

# Reporting
screenshot.on.failure=true
extent.report.path=reports/extent-reports

ConfigReader.java:

package com.automation.config;

import java.io.FileInputStream;
import java.util.Properties;

public class ConfigReader {
    private static Properties properties;
    
    static {
        try {
            FileInputStream fis = new FileInputStream("src/test/resources/config.properties");
            properties = new Properties();
            properties.load(fis);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public static String getProperty(String key) {
        return properties.getProperty(key);
    }
    
    public static String getBaseUrl() {
        return properties.getProperty("base.url");
    }
    
    public static String getBrowser() {
        return properties.getProperty("browser", "chrome");
    }
    
    public static int getTimeout() {
        return Integer.parseInt(properties.getProperty("app.timeout", "10"));
    }
    
    public static boolean isHeadless() {
        return Boolean.parseBoolean(properties.getProperty("headless", "false"));
    }
}

Driver Factory Pattern

DriverFactory.java:

package com.automation.config;

import io.github.bonigarcia.wdm.WebDriverManager;
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.remote.RemoteWebDriver;
import java.net.URL;
import java.time.Duration;

public class DriverFactory {
    private static ThreadLocal<WebDriver> driver = new ThreadLocal<>();
    
    public static WebDriver getDriver() {
        return driver.get();
    }
    
    public static void setDriver(WebDriver driverInstance) {
        driver.set(driverInstance);
    }
    
    public static void initializeDriver(String browser) {
        WebDriver webDriver;
        
        if (ConfigReader.getProperty("grid.enabled").equals("true")) {
            webDriver = initializeRemoteDriver(browser);
        } else {
            webDriver = initializeLocalDriver(browser);
        }
        
        webDriver.manage().window().maximize();
        webDriver.manage().timeouts().implicitlyWait(
            Duration.ofSeconds(ConfigReader.getTimeout())
        );
        
        setDriver(webDriver);
    }
    
    private static WebDriver initializeLocalDriver(String browser) {
        WebDriver webDriver;
        
        switch (browser.toLowerCase()) {
            case "chrome":
                WebDriverManager.chromedriver().setup();
                ChromeOptions chromeOptions = new ChromeOptions();
                if (ConfigReader.isHeadless()) {
                    chromeOptions.addArguments("--headless");
                }
                chromeOptions.addArguments("--disable-notifications");
                chromeOptions.addArguments("--disable-popup-blocking");
                webDriver = new ChromeDriver(chromeOptions);
                break;
                
            case "firefox":
                WebDriverManager.firefoxdriver().setup();
                FirefoxOptions firefoxOptions = new FirefoxOptions();
                if (ConfigReader.isHeadless()) {
                    firefoxOptions.addArguments("--headless");
                }
                webDriver = new FirefoxDriver(firefoxOptions);
                break;
                
            case "edge":
                WebDriverManager.edgedriver().setup();
                webDriver = new EdgeDriver();
                break;
                
            default:
                throw new RuntimeException("Browser not supported: " + browser);
        }
        
        return webDriver;
    }
    
    private static WebDriver initializeRemoteDriver(String browser) {
        try {
            ChromeOptions options = new ChromeOptions();
            URL gridUrl = new URL(ConfigReader.getProperty("grid.url"));
            return new RemoteWebDriver(gridUrl, options);
        } catch (Exception e) {
            throw new RuntimeException("Failed to initialize remote driver", e);
        }
    }
    
    public static void quitDriver() {
        if (driver.get() != null) {
            driver.get().quit();
            driver.remove();
        }
    }
}

Base Test Class

BaseTest.java:

package com.automation.tests;

import com.automation.config.ConfigReader;
import com.automation.config.DriverFactory;
import org.openqa.selenium.WebDriver;
import org.testng.annotations.*;

public class BaseTest {
    protected WebDriver driver;
    
    @BeforeMethod
    @Parameters("browser")
    public void setUp(@Optional("chrome") String browser) {
        DriverFactory.initializeDriver(browser);
        driver = DriverFactory.getDriver();
        driver.get(ConfigReader.getBaseUrl());
    }
    
    @AfterMethod
    public void tearDown() {
        DriverFactory.quitDriver();
    }
}

Screenshot Utility

ScreenshotUtil.java:

package com.automation.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 {
    
    public static String captureScreenshot(WebDriver driver, String testName) {
        try {
            TakesScreenshot ts = (TakesScreenshot) driver;
            File source = ts.getScreenshotAs(OutputType.FILE);
            
            String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
            String fileName = testName + "_" + timestamp + ".png";
            String destination = "screenshots/" + fileName;
            
            File finalDestination = new File(destination);
            FileUtils.copyFile(source, finalDestination);
            
            return destination;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    
    public static String captureFullPageScreenshot(WebDriver driver, String testName) {
        try {
            // Take full page screenshot (Chrome only)
            ChromeDriver chromeDriver = (ChromeDriver) driver;
            File source = chromeDriver.getScreenshotAs(OutputType.FILE);
            
            String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
            String destination = "screenshots/" + testName + "_full_" + timestamp + ".png";
            
            FileUtils.copyFile(source, new File(destination));
            return destination;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

Test Listener with Reporting

TestListener.java:

package com.automation.listeners;

import com.automation.config.DriverFactory;
import com.automation.utils.ScreenshotUtil;
import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.Status;
import com.aventstack.extentreports.reporter.ExtentSparkReporter;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;

public class TestListener implements ITestListener {
    private static ExtentReports extent;
    private static ThreadLocal<ExtentTest> test = new ThreadLocal<>();
    
    @Override
    public void onStart(ITestContext context) {
        ExtentSparkReporter reporter = new ExtentSparkReporter(
            "reports/extent-reports/test-report.html"
        );
        reporter.config().setDocumentTitle("Test Automation Report");
        reporter.config().setReportName("Functional Testing");
        
        extent = new ExtentReports();
        extent.attachReporter(reporter);
        extent.setSystemInfo("Environment", "QA");
        extent.setSystemInfo("Tester", System.getProperty("user.name"));
    }
    
    @Override
    public void onTestStart(ITestResult result) {
        ExtentTest extentTest = extent.createTest(result.getMethod().getMethodName());
        test.set(extentTest);
    }
    
    @Override
    public void onTestSuccess(ITestResult result) {
        test.get().log(Status.PASS, "Test passed");
    }
    
    @Override
    public void onTestFailure(ITestResult result) {
        test.get().log(Status.FAIL, "Test failed");
        test.get().log(Status.FAIL, result.getThrowable());
        
        String screenshot = ScreenshotUtil.captureScreenshot(
            DriverFactory.getDriver(),
            result.getMethod().getMethodName()
        );
        
        if (screenshot != null) {
            test.get().addScreenCaptureFromPath(screenshot);
        }
    }
    
    @Override
    public void onTestSkipped(ITestResult result) {
        test.get().log(Status.SKIP, "Test skipped");
        test.get().log(Status.SKIP, result.getThrowable());
    }
    
    @Override
    public void onFinish(ITestContext context) {
        extent.flush();
    }
}

Logging Framework

pom.xml (add log4j2):

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.20.0</version>
</dependency>

log4j2.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
        
        <File name="File" fileName="logs/automation.log">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
        </File>
    </Appenders>
    
    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="File"/>
        </Root>
    </Loggers>
</Configuration>

Usage in tests:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class LoginTest extends BaseTest {
    private static final Logger log = LogManager.getLogger(LoginTest.class);
    
    @Test
    public void testLogin() {
        log.info("Starting login test");
        
        LoginPage loginPage = new LoginPage(driver);
        loginPage.login("user", "pass");
        
        log.info("Login successful");
    }
}

Performance Optimization

public class PerformanceOptimization {
    
    // ❌ Bad - Multiple database calls
    @Test
    public void inefficientTest() {
        for (int i = 0; i < 10; i++) {
            driver.findElement(By.id("item-" + i)).click();
            // Each iteration makes a findElement call
        }
    }
    
    // ✅ Good - Find once, use multiple times
    @Test
    public void efficientTest() {
        List<WebElement> items = driver.findElements(By.className("item"));
        for (WebElement item : items) {
            item.click();
        }
    }
    
    // Use CSS Selectors over XPath
    // ❌ Slower
    driver.findElement(By.xpath("//div[@class='container']/input[@id='username']"));
    
    // ✅ Faster
    driver.findElement(By.cssSelector("div.container input#username"));
    
    // Disable images for faster page load
    ChromeOptions options = new ChromeOptions();
    Map<String, Object> prefs = new HashMap<>();
    prefs.put("profile.managed_default_content_settings.images", 2);
    options.setExperimentalOption("prefs", prefs);
    WebDriver driver = new ChromeDriver(options);
}

Retry Logic

import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;

public class RetryAnalyzer implements IRetryAnalyzer {
    private int retryCount = 0;
    private static final int maxRetryCount = 2;
    
    @Override
    public boolean retry(ITestResult result) {
        if (retryCount < maxRetryCount) {
            retryCount++;
            return true;
        }
        return false;
    }
}

// Usage
@Test(retryAnalyzer = RetryAnalyzer.class)
public void flakyTest() {
    // Test that might fail intermittently
}

Constants Management

AppConstants.java:

package com.automation.constants;

public class AppConstants {
    // Timeouts
    public static final int EXPLICIT_WAIT = 10;
    public static final int PAGE_LOAD_TIMEOUT = 30;
    public static final int IMPLICIT_WAIT = 5;
    
    // URLs
    public static final String LOGIN_URL = "/login";
    public static final String DASHBOARD_URL = "/dashboard";
    
    // Test Data
    public static final String VALID_USERNAME = "testuser@example.com";
    public static final String VALID_PASSWORD = "Test@123";
    
    // Messages
    public static final String LOGIN_SUCCESS_MSG = "Welcome back!";
    public static final String LOGIN_ERROR_MSG = "Invalid credentials";
}

Best Practices Checklist

  1. ✅ Use Page Object Model - Separate page logic from tests
  2. ✅ Implement Driver Factory - Centralized driver management
  3. ✅ Configuration Management - External properties files
  4. ✅ Use ThreadLocal for parallel execution - Thread-safe drivers
  5. ✅ Implement proper waits - Explicit waits over implicit
  6. ✅ Capture screenshots on failure - Debugging aid
  7. ✅ Use logging - Track test execution
  8. ✅ Implement retry logic - Handle flaky tests
  9. ✅ Follow naming conventions - Clear, descriptive names
  10. ✅ Keep tests independent - No dependencies between tests

Key Takeaways

✅ Framework structure enables scalability and maintainability
✅ Configuration management simplifies environment changes
✅ Proper reporting and logging aid debugging
✅ ThreadLocal ensures thread safety for parallel execution

What is the purpose of ThreadLocal in Selenium?

To speed up test execution
To share data between tests
To maintain separate driver instances for parallel tests
To reduce memory usage

Which locator strategy is generally fastest?

CSS Selector
XPath
Link Text
Class Name