Hybrid Framework Architecture

Enterprise-grade framework from scratch

What is a Hybrid Framework?

A Hybrid Framework combines the strengths of multiple framework types - Page Object Model, Data-Driven, Keyword-Driven, and BDD - into a unified, scalable architecture.

Complete Project Structure

automation-framework/
├── src/
│   ├── main/java/
│   │   └── com/framework/
│   │       ├── config/
│   │       │   ├── ConfigReader.java
│   │       │   └── DriverConfig.java
│   │       ├── driver/
│   │       │   ├── DriverFactory.java
│   │       │   └── DriverManager.java
│   │       ├── pages/
│   │       │   ├── BasePage.java
│   │       │   ├── LoginPage.java
│   │       │   └── DashboardPage.java
│   │       ├── utils/
│   │       │   ├── WaitUtils.java
│   │       │   ├── ElementUtils.java
│   │       │   ├── ScreenshotUtils.java
│   │       │   └── DateUtils.java
│   │       └── constants/
│   │           └── FrameworkConstants.java
│   └── test/
│       ├── java/
│       │   └── com/tests/
│       │       ├── base/
│       │       │   └── BaseTest.java
│       │       ├── login/
│       │       │   └── LoginTests.java
│       │       └── listeners/
│       │           └── TestListener.java
│       └── resources/
│           ├── config/
│           │   ├── config.properties
│           │   └── config-qa.properties
│           ├── testdata/
│           │   └── testdata.json
│           └── testng.xml
├── reports/
├── logs/
├── screenshots/
└── pom.xml

Configuration Management

Framework Constants

Centralize all constants to avoid hardcoding:

public final class FrameworkConstants {
    private FrameworkConstants() {} // Prevent instantiation

    // Paths
    public static final String CONFIG_PATH = "src/test/resources/config/";
    public static final String TESTDATA_PATH = "src/test/resources/testdata/";
    public static final String SCREENSHOT_PATH = "screenshots/";
    public static final String REPORT_PATH = "reports/";

    // Timeouts
    public static final int EXPLICIT_WAIT = 10;
    public static final int PAGE_LOAD_TIMEOUT = 30;
    public static final int IMPLICIT_WAIT = 0;

    // Environment
    public static final String ENV = System.getProperty("env", "qa");
}

Config Properties File

# config.properties
browser=chrome
headless=false
baseUrl=https://example.com

# Timeouts
explicitWait=10
pageLoadTimeout=30

# Reporting
screenshotOnFailure=true
videoRecording=false

# Grid
useGrid=false
gridUrl=http://localhost:4444

Configuration Reader

public class ConfigReader {
    private static Properties properties;
    private static final String CONFIG_FILE = FrameworkConstants.CONFIG_PATH + "config.properties";

    static {
        loadProperties();
    }

    private static void loadProperties() {
        properties = new Properties();
        try (InputStream input = new FileInputStream(CONFIG_FILE)) {
            properties.load(input);
        } catch (IOException e) {
            throw new FrameworkException("Failed to load config: " + e.getMessage());
        }

        // Override with environment-specific config if exists
        String env = FrameworkConstants.ENV;
        String envConfigFile = FrameworkConstants.CONFIG_PATH + "config-" + env + ".properties";
        if (new File(envConfigFile).exists()) {
            try (InputStream input = new FileInputStream(envConfigFile)) {
                properties.load(input);
            } catch (IOException e) {
                // Log warning but continue
            }
        }
    }

    public static String get(String key) {
        // System property overrides config file
        String systemProp = System.getProperty(key);
        return systemProp != null ? systemProp : properties.getProperty(key);
    }

    public static String get(String key, String defaultValue) {
        String value = get(key);
        return value != null ? value : defaultValue;
    }

    public static int getInt(String key, int defaultValue) {
        String value = get(key);
        return value != null ? Integer.parseInt(value) : defaultValue;
    }

    public static boolean getBoolean(String key) {
        return Boolean.parseBoolean(get(key));
    }
}

Driver Management

Driver Factory Pattern

Create drivers based on browser type with proper options:

public class DriverFactory {

    public static WebDriver createDriver(String browser) {
        WebDriver driver;
        boolean headless = ConfigReader.getBoolean("headless");

        switch (browser.toLowerCase()) {
            case "chrome":
                driver = createChromeDriver(headless);
                break;
            case "firefox":
                driver = createFirefoxDriver(headless);
                break;
            case "edge":
                driver = createEdgeDriver(headless);
                break;
            default:
                throw new FrameworkException("Unsupported browser: " + browser);
        }

        configureDriver(driver);
        return driver;
    }

    private static WebDriver createChromeDriver(boolean headless) {
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--start-maximized", "--disable-notifications");
        if (headless) {
            options.addArguments("--headless=new");
        }
        return new ChromeDriver(options);
    }

    private static WebDriver createFirefoxDriver(boolean headless) {
        FirefoxOptions options = new FirefoxOptions();
        if (headless) {
            options.addArguments("--headless");
        }
        return new FirefoxDriver(options);
    }

    private static WebDriver createEdgeDriver(boolean headless) {
        EdgeOptions options = new EdgeOptions();
        if (headless) {
            options.addArguments("--headless");
        }
        return new EdgeDriver(options);
    }

    private static void configureDriver(WebDriver driver) {
        driver.manage().timeouts().pageLoadTimeout(
            Duration.ofSeconds(ConfigReader.getInt("pageLoadTimeout", 30))
        );
        driver.manage().window().maximize();
    }
}

Thread-Safe Driver Manager

Use ThreadLocal for parallel test execution:

public class DriverManager {
    private static final ThreadLocal<WebDriver> driverThreadLocal = new ThreadLocal<>();

    private DriverManager() {}

    public static void initDriver() {
        if (driverThreadLocal.get() == null) {
            String browser = ConfigReader.get("browser", "chrome");

            WebDriver driver;
            if (ConfigReader.getBoolean("useGrid")) {
                driver = createRemoteDriver(browser);
            } else {
                driver = DriverFactory.createDriver(browser);
            }
            driverThreadLocal.set(driver);
        }
    }

    public static WebDriver getDriver() {
        WebDriver driver = driverThreadLocal.get();
        if (driver == null) {
            throw new FrameworkException("Driver not initialized. Call initDriver() first.");
        }
        return driver;
    }

    public static void quitDriver() {
        WebDriver driver = driverThreadLocal.get();
        if (driver != null) {
            driver.quit();
            driverThreadLocal.remove();
        }
    }

    private static WebDriver createRemoteDriver(String browser) {
        String gridUrl = ConfigReader.get("gridUrl");
        try {
            ChromeOptions options = new ChromeOptions(); // Simplified
            return new RemoteWebDriver(new URL(gridUrl), options);
        } catch (MalformedURLException e) {
            throw new FrameworkException("Invalid Grid URL: " + gridUrl);
        }
    }
}

Page Object Layer

Base Page with Common Methods

public abstract class BasePage {
    protected WebDriver driver;
    protected WebDriverWait wait;

    public BasePage() {
        this.driver = DriverManager.getDriver();
        this.wait = new WebDriverWait(driver,
            Duration.ofSeconds(FrameworkConstants.EXPLICIT_WAIT));
        PageFactory.initElements(driver, this);
    }

    // Wait utilities
    protected WebElement waitForVisible(WebElement element) {
        return wait.until(ExpectedConditions.visibilityOf(element));
    }

    protected WebElement waitForClickable(WebElement element) {
        return wait.until(ExpectedConditions.elementToBeClickable(element));
    }

    protected void waitForInvisible(WebElement element) {
        wait.until(ExpectedConditions.invisibilityOf(element));
    }

    // Action methods
    protected void click(WebElement element) {
        waitForClickable(element).click();
    }

    protected void type(WebElement element, String text) {
        WebElement el = waitForVisible(element);
        el.clear();
        el.sendKeys(text);
    }

    protected String getText(WebElement element) {
        return waitForVisible(element).getText();
    }

    protected void selectByText(WebElement element, String text) {
        new Select(waitForVisible(element)).selectByVisibleText(text);
    }

    protected boolean isDisplayed(WebElement element) {
        try {
            return element.isDisplayed();
        } catch (NoSuchElementException | StaleElementReferenceException e) {
            return false;
        }
    }

    // Page navigation
    protected void navigateTo(String url) {
        driver.get(url);
    }

    public String getPageTitle() {
        return driver.getTitle();
    }

    public String getCurrentUrl() {
        return driver.getCurrentUrl();
    }
}

Page Object Implementation

public class LoginPage extends BasePage {

    @FindBy(id = "username")
    private WebElement usernameField;

    @FindBy(id = "password")
    private WebElement passwordField;

    @FindBy(id = "login-btn")
    private WebElement loginButton;

    @FindBy(css = ".error-message")
    private WebElement errorMessage;

    @FindBy(css = ".loading-spinner")
    private WebElement loadingSpinner;

    public LoginPage open() {
        navigateTo(ConfigReader.get("baseUrl") + "/login");
        return this;
    }

    public LoginPage enterUsername(String username) {
        type(usernameField, username);
        return this;
    }

    public LoginPage enterPassword(String password) {
        type(passwordField, password);
        return this;
    }

    public DashboardPage clickLogin() {
        click(loginButton);
        waitForInvisible(loadingSpinner);
        return new DashboardPage();
    }

    // Fluent method for complete login
    public DashboardPage loginAs(String username, String password) {
        return enterUsername(username)
               .enterPassword(password)
               .clickLogin();
    }

    public String getErrorMessage() {
        return getText(errorMessage);
    }

    public boolean isErrorDisplayed() {
        return isDisplayed(errorMessage);
    }
}

Utility Classes

Screenshot Utility

public class ScreenshotUtils {

    public static String capture(String testName) {
        WebDriver driver = DriverManager.getDriver();
        String timestamp = DateUtils.getTimestamp("yyyyMMdd_HHmmss");
        String fileName = testName + "_" + timestamp + ".png";
        String filePath = FrameworkConstants.SCREENSHOT_PATH + fileName;

        try {
            File srcFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
            File destFile = new File(filePath);
            destFile.getParentFile().mkdirs();
            Files.copy(srcFile.toPath(), destFile.toPath());
            return filePath;
        } catch (IOException e) {
            throw new FrameworkException("Failed to capture screenshot: " + e.getMessage());
        }
    }

    public static byte[] captureAsBytes() {
        return ((TakesScreenshot) DriverManager.getDriver())
               .getScreenshotAs(OutputType.BYTES);
    }

    public static String captureAsBase64() {
        return ((TakesScreenshot) DriverManager.getDriver())
               .getScreenshotAs(OutputType.BASE64);
    }
}

Wait Utility

public class WaitUtils {
    private static WebDriverWait getWait(int seconds) {
        return new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(seconds));
    }

    public static void waitForPageLoad() {
        getWait(30).until(driver ->
            ((JavascriptExecutor) driver)
            .executeScript("return document.readyState").equals("complete")
        );
    }

    public static void waitForAjax() {
        getWait(30).until(driver ->
            (Boolean) ((JavascriptExecutor) driver)
            .executeScript("return jQuery.active == 0")
        );
    }

    public static void waitForUrl(String urlContains) {
        getWait(10).until(ExpectedConditions.urlContains(urlContains));
    }

    public static void waitForTextInElement(WebElement element, String text) {
        getWait(10).until(ExpectedConditions.textToBePresentInElement(element, text));
    }

    public static void sleep(int milliseconds) {
        try {
            Thread.sleep(milliseconds);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

JavaScript Utility

public class JSUtils {
    private static JavascriptExecutor getJS() {
        return (JavascriptExecutor) DriverManager.getDriver();
    }

    public static void click(WebElement element) {
        getJS().executeScript("arguments[0].click();", element);
    }

    public static void scrollToElement(WebElement element) {
        getJS().executeScript("arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});", element);
    }

    public static void scrollToBottom() {
        getJS().executeScript("window.scrollTo(0, document.body.scrollHeight);");
    }

    public static void highlight(WebElement element) {
        getJS().executeScript(
            "arguments[0].style.border='3px solid red';", element
        );
    }

    public static void setAttribute(WebElement element, String attr, String value) {
        getJS().executeScript(
            "arguments[0].setAttribute(arguments[1], arguments[2]);",
            element, attr, value
        );
    }

    public static String getInnerText(WebElement element) {
        return (String) getJS().executeScript("return arguments[0].innerText;", element);
    }
}

Test Data Management

JSON Test Data

{
  "validUser": {
    "username": "testuser",
    "password": "Test@123",
    "email": "test@example.com"
  },
  "invalidUser": {
    "username": "invalid",
    "password": "wrong"
  },
  "products": [
    {"name": "Laptop", "price": 999.99},
    {"name": "Mouse", "price": 29.99}
  ]
}

Test Data Reader

public class TestDataReader {
    private static JsonObject testData;

    static {
        loadTestData();
    }

    private static void loadTestData() {
        String filePath = FrameworkConstants.TESTDATA_PATH + "testdata.json";
        try (Reader reader = new FileReader(filePath)) {
            testData = JsonParser.parseReader(reader).getAsJsonObject();
        } catch (IOException e) {
            throw new FrameworkException("Failed to load test data: " + e.getMessage());
        }
    }

    public static String get(String path) {
        // Supports nested paths like "validUser.username"
        String[] keys = path.split("\\.");
        JsonElement element = testData;

        for (String key : keys) {
            if (element.isJsonObject()) {
                element = element.getAsJsonObject().get(key);
            } else if (element.isJsonArray() && key.matches("\\d+")) {
                element = element.getAsJsonArray().get(Integer.parseInt(key));
            }
        }
        return element.getAsString();
    }

    public static <T> T getObject(String key, Class<T> clazz) {
        Gson gson = new Gson();
        return gson.fromJson(testData.get(key), clazz);
    }

    public static List<Map<String, Object>> getList(String key) {
        Gson gson = new Gson();
        Type type = new TypeToken<List<Map<String, Object>>>(){}.getType();
        return gson.fromJson(testData.get(key), type);
    }
}

Data Provider Pattern

public class TestDataProvider {

    @DataProvider(name = "loginData")
    public static Object[][] getLoginData() {
        return new Object[][] {
            {TestDataReader.get("validUser.username"),
             TestDataReader.get("validUser.password"), true},
            {TestDataReader.get("invalidUser.username"),
             TestDataReader.get("invalidUser.password"), false}
        };
    }

    @DataProvider(name = "loginDataFromJson", parallel = true)
    public static Object[][] getLoginDataFromJson() {
        List<Map<String, Object>> users = TestDataReader.getList("users");
        Object[][] data = new Object[users.size()][3];

        for (int i = 0; i 

Test Layer

Base Test Class

public class BaseTest {
    protected LoginPage loginPage;
    protected DashboardPage dashboardPage;

    @BeforeMethod
    public void setUp(Method method) {
        DriverManager.initDriver();
        Log.info("Starting test: " + method.getName());
    }

    @AfterMethod
    public void tearDown(ITestResult result) {
        if (result.getStatus() == ITestResult.FAILURE) {
            Log.error("Test failed: " + result.getName());
            if (ConfigReader.getBoolean("screenshotOnFailure")) {
                String path = ScreenshotUtils.capture(result.getName());
                Log.info("Screenshot saved: " + path);
            }
        }
        DriverManager.quitDriver();
    }

    @BeforeSuite
    public void suiteSetup() {
        // Clean up old reports/screenshots
        FileUtils.cleanDirectory(FrameworkConstants.SCREENSHOT_PATH);
        Log.info("Test suite started");
    }

    @AfterSuite
    public void suiteTeardown() {
        Log.info("Test suite completed");
    }
}

Test Class Implementation

@Listeners(TestListener.class)
public class LoginTests extends BaseTest {

    @BeforeMethod
    public void navigateToLogin() {
        loginPage = new LoginPage().open();
    }

    @Test(description = "Verify successful login with valid credentials")
    public void testValidLogin() {
        dashboardPage = loginPage.loginAs(
            TestDataReader.get("validUser.username"),
            TestDataReader.get("validUser.password")
        );

        Assert.assertTrue(dashboardPage.isWelcomeDisplayed(),
            "Dashboard should display welcome message");
        Assert.assertEquals(dashboardPage.getLoggedInUser(),
            TestDataReader.get("validUser.username"));
    }

    @Test(description = "Verify error message for invalid credentials")
    public void testInvalidLogin() {
        loginPage.enterUsername("invalid")
                 .enterPassword("wrong");
        loginPage.clickLoginExpectingFailure();

        Assert.assertTrue(loginPage.isErrorDisplayed(),
            "Error message should be displayed");
        Assert.assertEquals(loginPage.getErrorMessage(),
            "Invalid username or password");
    }

    @Test(dataProvider = "loginData", dataProviderClass = TestDataProvider.class)
    public void testLoginWithDataProvider(String username, String password, boolean shouldPass) {
        loginPage.enterUsername(username)
                 .enterPassword(password);

        if (shouldPass) {
            dashboardPage = loginPage.clickLogin();
            Assert.assertTrue(dashboardPage.isWelcomeDisplayed());
        } else {
            loginPage.clickLoginExpectingFailure();
            Assert.assertTrue(loginPage.isErrorDisplayed());
        }
    }
}

Logging

Log4j2 Configuration

<!-- log4j2.xml -->
<Configuration status="WARN">
    <Properties>
        <Property name="logPath">logs</Property>
        <Property name="pattern">%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n</Property>
    </Properties>

    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="${pattern}"/>
        </Console>
        <RollingFile name="File" fileName="${logPath}/automation.log"
                     filePattern="${logPath}/automation-%d{yyyy-MM-dd}.log">
            <PatternLayout pattern="${pattern}"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
            </Policies>
        </RollingFile>
    </Appenders>

    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="File"/>
        </Root>
    </Loggers>
</Configuration>

Log Utility Wrapper

public class Log {
    private static final Logger logger = LogManager.getLogger(Log.class);

    public static void info(String message) {
        logger.info(message);
    }

    public static void debug(String message) {
        logger.debug(message);
    }

    public static void warn(String message) {
        logger.warn(message);
    }

    public static void error(String message) {
        logger.error(message);
    }

    public static void error(String message, Throwable t) {
        logger.error(message, t);
    }

    // Test lifecycle logging
    public static void startTest(String testName) {
        logger.info("========== STARTING TEST: {} ==========", testName);
    }

    public static void endTest(String testName, String status) {
        logger.info("========== TEST {}: {} ==========", testName, status);
    }

    public static void step(String stepDescription) {
        logger.info("STEP: {}", stepDescription);
    }
}

Test Listeners

TestNG Listener

public class TestListener implements ITestListener, ISuiteListener {

    @Override
    public void onStart(ISuite suite) {
        Log.info("Suite started: " + suite.getName());
    }

    @Override
    public void onFinish(ISuite suite) {
        Log.info("Suite finished: " + suite.getName());
    }

    @Override
    public void onTestStart(ITestResult result) {
        Log.startTest(result.getName());
    }

    @Override
    public void onTestSuccess(ITestResult result) {
        Log.endTest(result.getName(), "PASSED");
    }

    @Override
    public void onTestFailure(ITestResult result) {
        Log.error("Test failed: " + result.getThrowable().getMessage());

        // Capture screenshot
        String screenshotPath = ScreenshotUtils.capture(result.getName());
        Log.info("Screenshot: " + screenshotPath);

        // Attach to report (if using Allure)
        attachScreenshot(result.getName());

        Log.endTest(result.getName(), "FAILED");
    }

    @Override
    public void onTestSkipped(ITestResult result) {
        Log.warn("Test skipped: " + result.getName());
        Log.endTest(result.getName(), "SKIPPED");
    }

    private void attachScreenshot(String name) {
        try {
            Allure.addAttachment(name, new ByteArrayInputStream(
                ScreenshotUtils.captureAsBytes()
            ));
        } catch (Exception e) {
            Log.warn("Could not attach screenshot to report");
        }
    }
}

Retry Analyzer

public class RetryAnalyzer implements IRetryAnalyzer {
    private int retryCount = 0;
    private static final int MAX_RETRY = 2;

    @Override
    public boolean retry(ITestResult result) {
        if (retryCount 

Reporting Integration

Extent Reports Setup

public class ExtentManager {
    private static ExtentReports extent;
    private static ThreadLocal<ExtentTest> test = new ThreadLocal<>();

    public static ExtentReports getInstance() {
        if (extent == null) {
            String reportPath = FrameworkConstants.REPORT_PATH + "ExtentReport.html";
            ExtentSparkReporter spark = new ExtentSparkReporter(reportPath);
            spark.config().setTheme(Theme.DARK);
            spark.config().setDocumentTitle("Automation Report");
            spark.config().setReportName("Test Execution Report");

            extent = new ExtentReports();
            extent.attachReporter(spark);
            extent.setSystemInfo("Environment", FrameworkConstants.ENV);
            extent.setSystemInfo("Browser", ConfigReader.get("browser"));
        }
        return extent;
    }

    public static ExtentTest createTest(String name, String description) {
        ExtentTest extentTest = getInstance().createTest(name, description);
        test.set(extentTest);
        return extentTest;
    }

    public static ExtentTest getTest() {
        return test.get();
    }

    public static void flush() {
        if (extent != null) {
            extent.flush();
        }
    }

    public static void logPass(String message) {
        getTest().pass(message);
    }

    public static void logFail(String message) {
        getTest().fail(message);
    }

    public static void logInfo(String message) {
        getTest().info(message);
    }

    public static void attachScreenshot() {
        getTest().addScreenCaptureFromBase64String(
            ScreenshotUtils.captureAsBase64(), "Screenshot"
        );
    }
}

CI/CD Integration

Maven Configuration

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.2</version>
            <configuration>
                <suiteXmlFiles>
                    <suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
                </suiteXmlFiles>
                <systemPropertyVariables>
                    <browser>${browser}</browser>
                    <env>${env}</env>
                </systemPropertyVariables>
            </configuration>
        </plugin>
    </plugins>
</build>

GitHub Actions Workflow

name: Automation Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 6 * * *'  # Daily at 6 AM

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Cache Maven packages
        uses: actions/cache@v3
        with:
          path: ~/.m2
          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}

      - name: Run Tests
        run: mvn test -Dbrowser=chrome -Dheadless=true -Denv=qa

      - name: Upload Reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-reports
          path: |
            reports/
            screenshots/
            target/surefire-reports/

Jenkins Pipeline

pipeline {
    agent any

    parameters {
        choice(name: 'BROWSER', choices: ['chrome', 'firefox'], description: 'Browser')
        choice(name: 'ENV', choices: ['qa', 'staging', 'prod'], description: 'Environment')
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Test') {
            steps {
                sh "mvn test -Dbrowser=${params.BROWSER} -Denv=${params.ENV} -Dheadless=true"
            }
        }
    }

    post {
        always {
            publishHTML(target: [
                reportDir: 'reports',
                reportFiles: 'ExtentReport.html',
                reportName: 'Test Report'
            ])
            archiveArtifacts artifacts: 'screenshots/**/*', allowEmptyArchive: true
        }
    }
}

Best Practices

Framework Checklist

Next Steps