E-Commerce Framework
A framework that only runs tests isn't finished. This part of the e-commerce framework series adds the features that make it production-ready: Extent reports, Log4j2 logging, thread-safe parallel execution, screenshots on failure, JSON test data and a Jenkins pipeline. It builds on Building an E-Commerce Framework Step by Step.
Where Each Piece Lives
src/main/java/com/shop/
├── base/ DriverFactory.java ← ThreadLocal WebDriver
├── pages/ LoginPage, SearchPage, CartPage, CheckoutPage
└── utils/ ScreenshotUtil, ExtentManager, JsonDataReader, ConfigReader
src/test/java/com/shop/
├── tests/ BaseTest, LoginTest, SearchTest, CartTest, CheckoutTest
└── listeners/ TestListener.java ← reports + screenshots
src/test/resources/
├── testdata/ users.json, products.json
├── log4j2.xml
└── testng.xml
Jenkinsfile · pom.xml
1. Thread-Safe Parallel Execution
Parallel runs cut execution time, but only if every thread has its own browser. A shared static WebDriver makes tests click in each other's browsers.
public final class DriverFactory {
private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
public static void init(String browser) {
DRIVER.set("firefox".equals(browser) ? new FirefoxDriver() : new ChromeDriver());
DRIVER.get().manage().window().maximize();
}
public static WebDriver get() { return DRIVER.get(); }
public static void quit() {
if (DRIVER.get() != null) { DRIVER.get().quit(); DRIVER.remove(); }
}
}
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="EcommerceSuite" parallel="classes" thread-count="3">
<listeners>
<listener class-name="com.shop.listeners.TestListener"/>
</listeners>
<test name="Regression">
<parameter name="browser" value="chrome"/>
<classes>
<class name="com.shop.tests.LoginTest"/>
<class name="com.shop.tests.SearchTest"/>
<class name="com.shop.tests.CartTest"/>
<class name="com.shop.tests.CheckoutTest"/>
</classes>
</test>
</suite>
parallel="classes" runs the four classes on up to three threads. For cross-browser runs, use one <test> per browser and parallel="tests". Also make test data unique per test (for example, a new user per checkout) so threads don't fight over the same cart. More: Parallel Execution & Grid.
2. Extent Reports
Extent produces an HTML report with pass/fail status, steps, screenshots, timings and environment details — something you can send to a product owner.
public final class ExtentManager {
private static final ExtentReports EXTENT = create();
private static final ThreadLocal<ExtentTest> TEST = new ThreadLocal<>();
private static ExtentReports create() {
ExtentSparkReporter spark = new ExtentSparkReporter("reports/ExtentReport.html");
spark.config().setDocumentTitle("E-Commerce Regression");
ExtentReports extent = new ExtentReports();
extent.attachReporter(spark);
extent.setSystemInfo("Environment", ConfigReader.get("env"));
extent.setSystemInfo("Browser", ConfigReader.get("browser"));
return extent;
}
public static void startTest(String name) { TEST.set(EXTENT.createTest(name)); }
public static ExtentTest test() { return TEST.get(); }
public static void flush() { EXTENT.flush(); }
}
The ThreadLocal<ExtentTest> matters: in parallel runs, each thread logs to its own test entry instead of mixing steps between tests.
3. A Listener That Ties It Together
Instead of repeating reporting code in every test, a TestNG listener reacts to test events:
public class TestListener implements ITestListener {
private static final Logger LOG = LogManager.getLogger(TestListener.class);
@Override public void onTestStart(ITestResult r) {
ExtentManager.startTest(r.getMethod().getMethodName());
LOG.info("START {}", r.getName());
}
@Override public void onTestSuccess(ITestResult r) {
ExtentManager.test().pass("Test passed");
LOG.info("PASS {}", r.getName());
}
@Override public void onTestFailure(ITestResult r) {
String base64 = ScreenshotUtil.asBase64(DriverFactory.get());
ExtentManager.test().fail(r.getThrowable(),
MediaEntityBuilder.createScreenCaptureFromBase64String(base64).build());
ScreenshotUtil.saveToFile(DriverFactory.get(), r.getName());
LOG.error("FAIL {}: {}", r.getName(), r.getThrowable().getMessage());
}
@Override public void onTestSkipped(ITestResult r) {
ExtentManager.test().skip("Skipped: " + r.getThrowable());
}
@Override public void onFinish(ITestContext c) { ExtentManager.flush(); }
}
Watch the order: the screenshot needs a live browser. Depending on the TestNG version and setup, @AfterMethod may already have quit the driver by the time onTestFailure runs. If you see a null or closed driver in the listener, capture the screenshot in @AfterMethod (checking result.getStatus() == ITestResult.FAILURE) before calling DriverFactory.quit().
4. Screenshots on Failure
public final class ScreenshotUtil {
public static String asBase64(WebDriver driver) {
return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BASE64);
}
public static Path saveToFile(WebDriver driver, String testName) {
try {
String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
Path dest = Path.of("screenshots", testName + "_" + time + ".png");
Files.createDirectories(dest.getParent());
File src = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
return Files.copy(src.toPath(), dest, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new UncheckedIOException("Could not save screenshot", e);
}
}
}
-
Timestamps stop reruns overwriting earlier evidence.
-
Base64 embeds the image in the HTML report, so the report works when it's emailed or archived in Jenkins.
-
Failures only — screenshots of passing tests slow runs and bloat reports.
5. Log4j2 Logging
Logs show what happened when nobody was watching the browser — essential in CI.
<?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 %c{1} - %msg%n"/>
</Console>
<RollingFile name="File" fileName="logs/automation.log"
filePattern="logs/automation-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %c{1} - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="10 MB"/>
</Policies>
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>
The [%t] thread name in the pattern is what makes parallel-run logs readable. In page objects, log actions at info ("Adding 'Laptop' to cart") and details at debug. Never log passwords or tokens.
6. Test Data From JSON
[
{ "username": "standard_user", "password": "secret_sauce", "product": "Backpack" },
{ "username": "problem_user", "password": "secret_sauce", "product": "Bike Light" }
]
public static Object[][] readUsers() throws IOException {
List<Map<String, String>> rows = new ObjectMapper().readValue(
Path.of("src/test/resources/testdata/users.json").toFile(),
new TypeReference<>() {});
return rows.stream().map(r -> new Object[]{ r }).toArray(Object[][]::new);
}
@DataProvider(name = "users")
public Object[][] users() throws IOException { return JsonDataReader.readUsers(); }
@Test(dataProvider = "users")
public void addToCart(Map<String, String> user) { /* login, add user.get("product") */ }
Excel works the same way through Apache POI — see Data-Driven Testing. Keep real credentials in environment variables or Jenkins credentials, not in the data files.
7. Jenkins Pipeline
pipeline {
agent any
parameters {
choice(name: 'ENV', choices: ['qa', 'staging'], description: 'Environment')
choice(name: 'SUITE', choices: ['testng.xml', 'smoke.xml'], description: 'Suite')
}
triggers { cron('H 1 * * 1-5') } // nightly on weekdays
stages {
stage('Checkout') { steps { checkout scm } }
stage('Test') {
steps {
sh "mvn clean test -Denv=${params.ENV} -Dheadless=true -Dsurefire.suiteXmlFiles=${params.SUITE}"
}
}
}
post {
always {
junit 'target/surefire-reports/*.xml'
publishHTML(target: [reportDir: 'reports', reportFiles: 'ExtentReport.html',
reportName: 'Extent Report', keepAll: true, allowMissing: true,
alwaysLinkToLastBuild: true])
archiveArtifacts artifacts: 'screenshots/**, logs/**', allowEmptyArchive: true
}
}
}
Smoke runs on every push and full regression nightly. For consistent browsers and more parallel capacity, point the suite at a Selenium Grid running in Docker — see the Docker + Selenium Grid Guide — and the full Jenkins setup in The Complete Jenkins Tutorial.
What This Gives You
| Feature | Benefit |
|---|---|
| ThreadLocal driver + parallel classes | Much shorter runs without random cross-thread failures |
| Extent report | A shareable result with steps, screenshots and environment |
| Screenshots on failure | Evidence of what the page looked like when it failed |
| Log4j2 with thread names | Debugging CI failures without re-running |
| JSON/Excel data | New scenarios without code changes |
| Jenkins pipeline | Automatic smoke and nightly regression, with history |
From Real Projects
With over four years in testing, at Voltstream Technologies and WizzyBox, the interviews I've prepared for focused as much on projects and process as on theory. For each question here, practice a short answer that includes one real example: an application you tested, a test type you performed, or part of the framework you worked on. Testing Testsigma — a platform where users write automated tests in plain English — meant thinking like its users, who are testers themselves. Describe how results reach the team — reports and notifications matter as much as the tests.
📚 Official documentation: Selenium Grid documentation · Selenium documentation
Frequently Asked Questions
Why use Extent Reports?
They turn raw TestNG results into a readable HTML report with statuses, steps, screenshots, timings and environment details.
How do you make Extent Reports work in parallel?
Keep one shared ExtentReports object, but store each test's ExtentTest in a ThreadLocal, and flush once at the end.
How do you configure parallel execution?
Set parallel and thread-count in testng.xml, and use a ThreadLocal WebDriver so each thread has its own browser.
How are screenshots captured on failure?
In the listener's onTestFailure, or in @AfterMethod checking ITestResult.FAILURE — saved with a timestamp and embedded in the report as Base64.
What does the Jenkins pipeline do?
Checks out the code, runs Maven with environment and suite parameters, and publishes JUnit results, the Extent report, screenshots and logs.
Related