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
}
}
}