Selenium Java Automation Framework
– Step-by-Step Guide + Interview Q&A
Build a Java-based Selenium framework with TestNG, Maven, Log4j2, Extent Reports,
Cucumber BDD, REST Assured, Jenkins, and Selenium Grid on Docker. Includes code
skeletons and interview Q&A.
1) Recommended Folder Structure
selenium-java-framework/
├─ Jenkinsfile
├─ [Link]
├─ [Link]
├─ [Link]
├─ src
│ ├─ main/java/{base,driver,utils,listeners}
│ └─ test/java/{pages,tests,steps,runners,api}
└─ src/test/resources/{features,config,[Link],[Link]}
2) [Link] (Key Dependencies & Plugins)
<dependencies>
<dependency><groupId>[Link]</groupId><artifactId>selenium-
java</artifactId><version>4.23.0</version></dependency>
<dependency><groupId>[Link]</groupId><artifactId>testng</artifactId><version
>7.10.2</version><scope>test</scope></dependency>
<dependency><groupId>[Link]</groupId><artifactId>webdrivermanager<
/artifactId><version>5.8.0</version></dependency>
<dependency><groupId>[Link]</groupId><artifactId>cucumber-java</
artifactId><version>7.15.0</version><scope>test</scope></dependency>
<dependency><groupId>[Link]</groupId><artifactId>cucumber-testng</
artifactId><version>7.15.0</version><scope>test</scope></dependency>
<dependency><groupId>[Link]-assured</groupId><artifactId>rest-assured</
artifactId><version>5.4.0</version><scope>test</scope></dependency>
<dependency><groupId>[Link].log4j</groupId><artifactId>log4j-
core</artifactId><version>2.23.1</version></dependency>
<dependency><groupId>[Link]</groupId><artifactId>extentreports</
artifactId><version>5.1.1</version></dependency>
<dependency><groupId>[Link]</groupId><artifactId>poi-ooxml</
artifactId><version>5.2.5</version></dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>[Link]</groupId><artifactId>maven-surefire-
plugin</artifactId><version>3.2.5</version>
<configuration><suiteXmlFiles><suiteXmlFile>[Link]</suiteXmlFile></
suiteXmlFiles></configuration>
</plugin>
</plugins>
</build>
3) Configuration (src/test/resources/config/[Link])
baseUrl=[Link]
browser=chrome
headless=false
grid=false
gridUrl=[Link]
implicitWait=0
explicitWait=10
takeScreenshotOnFailure=true
Override properties at runtime:
mvn clean test -Dbrowser=edge -Denv=qa -Dgrid=true
-DgridUrl=[Link]
4) Thread-safe Driver Factory (local + Grid)
package driver;
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class DriverFactory {
private static ThreadLocal<WebDriver> tlDriver = new ThreadLocal<>();
private static ConcurrentHashMap<Long, String> threadToBrowser = new
ConcurrentHashMap<>();
public static WebDriver getDriver() { return [Link](); }
public static void setDriver(WebDriver driver) { [Link](driver); }
public static void unload() { WebDriver d = [Link](); if (d != null)
{ [Link](); [Link](); } }
public static void initDriver(String browser, boolean headless, boolean grid,
String gridUrl, int implicitWaitSec) {
try {
WebDriver driver;
switch ([Link]()) {
case "firefox": {
FirefoxOptions o = new FirefoxOptions(); if (headless)
[Link]("-headless");
if (grid) driver = new RemoteWebDriver(new URL(gridUrl), o); else
{ [Link]().setup(); driver = new FirefoxDriver(o);}
break; }
case "edge": {
EdgeOptions o = new EdgeOptions(); if (headless) [Link]("--
headless=new");
if (grid) driver = new RemoteWebDriver(new URL(gridUrl), o); else
{ [Link]().setup(); driver = new EdgeDriver(o);} break; }
default: {
ChromeOptions o = new ChromeOptions(); if (headless)
[Link]("--headless=new"); [Link]("--disable-gpu", "--window-
size=1920,1080");
if (grid) driver = new RemoteWebDriver(new URL(gridUrl), o); else
{ [Link]().setup(); driver = new ChromeDriver(o);} }
}
[Link]().timeouts().implicitlyWait([Link](implicitWaitSec));
[Link]().window().maximize();
setDriver(driver);
[Link]([Link]().getId(), browser);
} catch (Exception e) { throw new RuntimeException("Failed to init driver: "
+ [Link](), e); }
}
}
5) BaseTest with TestNG Hooks
package base;
import [Link];
import [Link].*;
import [Link];
import [Link];
public class BaseTest {
protected Properties config;
@Parameters({"browser", "headless", "grid", "gridUrl"})
@BeforeMethod(alwaysRun = true)
public void setUp(@Optional String browser, @Optional String headless,
@Optional String grid, @Optional String gridUrl) {
config = [Link]();
String br = [Link]("browser", browser != null ? browser :
[Link]("browser"));
boolean hl = [Link]([Link]("headless", headless !=
null ? headless : [Link]("headless")));
boolean useGrid = [Link]([Link]("grid", grid !=
null ? grid : [Link]("grid")));
String gUrl = [Link]("gridUrl", gridUrl != null ? gridUrl :
[Link]("gridUrl"));
int implicitWait = [Link]([Link]("implicitWait",
"0"));
[Link](br, hl, useGrid, gUrl, implicitWait);
}
@AfterMethod(alwaysRun = true)
public void tearDown() { [Link](); }
}
6) Config Reader Utility
package utils;
import [Link];
import [Link];
import [Link];
public class ConfigReader {
private static Properties props;
public static Properties getProperties() {
if (props == null) {
try (InputStream is = new
FileInputStream("src/test/resources/config/[Link]")) {
props = new Properties(); [Link](is);
} catch (Exception e) { throw new RuntimeException("Unable to load
[Link]", e); }
}
return props;
}
}
7) Wait & Screenshot Utilities
package utils;
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class WaitUtils {
private static final long EXPLICIT_WAIT =
[Link]([Link]().getProperty("explicitWait", "10"));
public static WebElement waitForVisible(By locator) {
return new WebDriverWait([Link](),
[Link](EXPLICIT_WAIT))
.until([Link](locator));
}
}
package utils;
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class ScreenshotUtil {
public static String takeScreenshot(String namePrefix) {
try {
File src = ((TakesScreenshot)
[Link]()).getScreenshotAs([Link]);
String ts = new SimpleDateFormat("yyyyMMdd_HHmmss_SSS").format(new
Date());
String path = "target/screenshots/" + namePrefix + "_" + ts + ".png";
File dest = new File(path); [Link]().mkdirs();
[Link](src, dest);
return [Link]();
} catch (Exception e) { return null; }
}
}
8) Page Object Model Example (Fluent Style)
package pages;
import [Link];
import [Link];
import [Link];
import [Link];
public class LoginPage {
private WebDriver driver = [Link]();
private By username = [Link]("username");
private By password = [Link]("password");
private By loginBtn = [Link]("button[type='submit']");
private By error = [Link](".error");
public LoginPage goTo(String url) { [Link](url); return this; }
public LoginPage typeUsername(String user)
{ [Link](username).sendKeys(user); return this; }
public LoginPage typePassword(String pass)
{ [Link](password).sendKeys(pass); return this; }
public HomePage clickLoginSuccess() { [Link](loginBtn).click();
return new HomePage(); }
public String getError() { return [Link](error).getText(); }
}
package pages;
import [Link];
import [Link];
public class HomePage {
private By header = [Link]("[Link]-title");
public String getHeaderText() { return
[Link]().findElement(header).getText(); }
}
9) TestNG Test with DataProvider (Excel)
package utils;
import [Link].*;
import [Link];
public class ExcelUtil {
public static Object[][] readSheet(String path, String sheetName) {
try (FileInputStream fis = new FileInputStream(path)) {
Workbook wb = [Link](fis); Sheet sheet =
[Link](sheetName);
int rows = [Link](); int cols =
[Link](0).getPhysicalNumberOfCells();
Object[][] data = new Object[rows-1][cols];
for (int i=1; i<rows; i++) { Row r = [Link](i); for (int j=0;
j<cols; j++) { Cell c = [Link](j); data[i-1][j] =
(c==null)?"":[Link](); } }
return data;
} catch (Exception e) { throw new RuntimeException("Excel read error:
"+[Link](), e); }
}
}
package tests;
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class LoginTest extends BaseTest {
@DataProvider(name="loginData", parallel=true)
public Object[][] loginData() { return
[Link]("test-data/[Link]", "Sheet1"); }
@Test(dataProvider="loginData", description="Validate login with multiple
credentials")
public void loginTest(String username, String password, String expected) {
String baseUrl = [Link]().getProperty("baseUrl");
HomePage home = new
LoginPage().goTo(baseUrl).typeUsername(username).typePassword(password).clickLog
inSuccess();
[Link]([Link](), expected);
}
}
10) [Link] for Parallel Cross-Browser
<!DOCTYPE suite SYSTEM "[Link] >
<suite name="UI Suite" parallel="tests" thread-count="3">
<test name="Chrome Suite">
<parameter name="browser" value="chrome"/>
<classes><class name="[Link]"/></classes>
</test>
<test name="Firefox Suite">
<parameter name="browser" value="firefox"/>
<classes><class name="[Link]"/></classes>
</test>
<test name="Edge Suite">
<parameter name="browser" value="edge"/>
<classes><class name="[Link]"/></classes>
</test>
</suite>
11) Log4j2 Configuration (src/test/resources/[Link])
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:[Link]} %-5level [%t] %c{1} - %msg%n"/>
</Console>
<RollingFile name="File" fileName="target/logs/[Link]"
filePattern="target/logs/test-%d{yyyy-MM-dd}-%[Link]">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="10 MB"/>
</Policies>
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>
12) Extent Reports Listener
package listeners;
import [Link];
import [Link];
import [Link];
import [Link];
import [Link].*;
import [Link];
public class ExtentReportListener implements ITestListener {
private static ExtentReports extent = createInstance();
private static ThreadLocal<ExtentTest> test = new ThreadLocal<>();
private static ExtentReports createInstance() {
ExtentSparkReporter spark = new
ExtentSparkReporter("target/extent-report/[Link]");
ExtentReports ext = new ExtentReports(); [Link](spark); return
ext;
}
public void onTestStart(ITestResult r)
{ [Link]([Link]([Link]().getMethodName())); }
public void onTestSuccess(ITestResult r) { [Link]().log([Link], "Test
passed"); }
public void onTestFailure(ITestResult r) { String p =
[Link]([Link]().getMethodName());
[Link]().fail([Link]()); if (p!=null) try
{ [Link]().addScreenCaptureFromPath(p);} catch (Exception e) {} }
public void onTestSkipped(ITestResult r) { [Link]().log([Link], "Test
skipped"); }
public void onFinish(ITestContext c) { [Link](); }
}
13) Cucumber BDD: Feature, Steps, Runner & Hooks
Feature: Login
As a user I want to login so that I can see my home page
Scenario Outline: Valid login
Given I navigate to the application
When I login with username "<user>" and password "<pass>"
Then I should see the home page title "<title>"
Examples:
| user | pass | title |
| standard1 | secret123 | My Home Page |
package steps;
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class LoginSteps extends BaseTest {
private HomePage home;
@Given("I navigate to the application")
public void i_navigate_to_the_application() {
String baseUrl = [Link]().getProperty("baseUrl");
new LoginPage().goTo(baseUrl);
}
@When("I login with username {string} and password {string}")
public void i_login_with_username_and_password(String user, String pass) {
home = new
LoginPage().typeUsername(user).typePassword(pass).clickLoginSuccess();
}
@Then("I should see the home page title {string}")
public void i_should_see_the_home_page_title(String title) {
[Link]([Link](), title);
}
}
package runners;
import [Link];
import [Link];
import [Link];
@CucumberOptions(
features = "src/test/resources/features",
glue = {"steps"},
plugin = {"pretty", "json:target/[Link]", "html:target/[Link]"},
monochrome = true
)
public class TestRunner extends AbstractTestNGCucumberTests {
@Override
@DataProvider(parallel = true)
public Object[][] scenarios() { return [Link](); }
}
14) REST Assured API Test (Same Suite)
package api;
import [Link];
import [Link];
import [Link];
import static [Link];
import static [Link].*;
public class UserApiTest {
@Test
public void createUser_shouldReturn201() {
[Link] = "[Link]
given().contentType([Link]).body("{\"name\":\"morpheus\", \"job\":\"le
ader\"}")
.when().post("/users")
.then().statusCode(201).body("name", equalTo("morpheus"));
}
}
15) Jenkins Declarative Pipeline (Jenkinsfile)
pipeline {
agent any
options { timestamps() }
parameters {
choice(name: 'BROWSER', choices: ['chrome','firefox','edge'], description:
'Browser to run')
booleanParam(name: 'GRID', defaultValue: false, description: 'Run on
Selenium Grid?')
string(name: 'GRID_URL', defaultValue: '[Link]
description: 'Grid URL')
booleanParam(name: 'HEADLESS', defaultValue: true, description: 'Headless
mode')
}
stages {
stage('Checkout') { steps { checkout scm } }
stage('Build') { steps { sh 'mvn -B -q -DskipTests clean compile' } }
stage('Test UI & API') {
steps {
sh 'mvn -B test -Dbrowser=${BROWSER} -Dgrid=${GRID} -DgridUrl=$
{GRID_URL} -Dheadless=${HEADLESS}'
}
post {
always {
junit 'target/surefire-reports/*.xml'
publishHTML(target: [allowMissing: true, alwaysLinkToLastBuild: true,
keepAll: true, reportDir: 'target/extent-report', reportFiles: '[Link]',
reportName: 'Extent Report'])
}
}
}
}
}
16) Selenium Grid v4 on Docker Compose ([Link])
version: '3.8'
services:
selenium-hub:
image: selenium/hub:4.21.0
container_name: selenium-hub
ports:
- '4442:4442'
- '4443:4443'
- '4444:4444'
chrome:
image: selenium/node-chrome:4.21.0
shm_size: '2gb'
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
firefox:
image: selenium/node-firefox:4.21.0
shm_size: '2gb'
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
edge:
image: selenium/node-edge:4.21.0
shm_size: '2gb'
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
17) How to Run (Local, Grid, Jenkins)
Local run (Chrome, headed):
mvn clean test -Dbrowser=chrome -Dheadless=false
Run via Selenium Grid (Docker):
docker compose up -d
# Wait for Grid health page: [Link]
mvn test -Dgrid=true -DgridUrl=[Link] -Dbrowser=chrome
18) Why BDD (Cucumber) and How It Fits
• Business-readable scenarios for collaboration.
• Step Definitions call Page Objects. Hooks (@Before/@After) reuse BaseTest to manage
drivers.
• Parallel at scenario level via TestNG DataProvider override in Cucumber runner.
• Reports: Cucumber HTML/JSON + (optional) Extent Cucumber adapter.
19) Interview Q&A – Selenium Framework (Senior/Lead)
Q1. How do you make WebDriver thread-safe for parallel execution?
Answer: Use ThreadLocal<WebDriver> and avoid static driver references.
Q2. Same test in different browsers at the same time?
Answer: Use TestNG suite with <test> per browser (parallel="tests").
Q3. Wait strategy best practices?
Answer: Keep implicit wait = 0; centralize explicit waits; avoid [Link]; use
WebDriverWait.
Q4. POM vs Page Factory vs Fluent?
Answer: Classic POM = By locators; Page Factory = @FindBy; Fluent POM returns next page
and allows chaining.
Q5. Failures & screenshots?
Answer: [Link] -> TakesScreenshot -> attach to Extent report.
Q6. Flaky test reduction?
Answer: Stable locators, proper waits, deterministic test data, optional retry.
Q7. Config & secrets in CI?
Answer: Properties + -D overrides; secrets via Jenkins credentials.
Q8. UI + API in one pipeline?
Answer: Yes; same Maven run or split by groups.
Q9. How to scale with Grid?
Answer: Hub + multiple nodes; tune TestNG thread-count and containers.
Q10. Data for parallel runs?
Answer: DataProvider(parallel=true); use independent rows or generate unique data.
Q11. Extent vs Allure?
Answer: Extent = easy HTML; Allure = trend/history; choose per standards.
Q12. Maintainability & reviews?
Answer: SRP, DRY, naming, avoid sleeps, linting, PR checks.
20) Bonus: RetryAnalyzer
package utils;
import [Link];
import [Link];
public class RetryAnalyzer implements IRetryAnalyzer {
private int count = 0;
private static final int MAX_RETRY = 1;
public boolean retry(ITestResult result) {
if (count < MAX_RETRY) { count++; return true; }
return false;
}
}