Building an E-Commerce Selenium Java Framework Step-by-Step
This guide builds the core of an e-commerce test framework in Selenium and Java — login, product list, cart, checkout and order confirmation — with complete, readable code for every layer. It targets SauceDemo, a store built for automation practice, so you can run everything yourself. For the 30-day plan around it (data-driven tests, reports, parallel runs, Jenkins), see Build a Framework From Scratch; for reporting, parallel runs and CI in detail, see E-Commerce Framework: Reporting, Parallel & CI/CD.
The Stack
| Tool | Role |
|---|---|
| Java 17+ | Language |
| Selenium WebDriver 4 | Browser automation (Selenium Manager handles drivers) |
| TestNG | Test runner, assertions, groups, DataProviders, parallel runs |
| Maven | Dependencies and builds — see Maven for Selenium |
| Page Object Model | Design pattern |
| Log4j2, Extent Reports, Apache POI | Logging, HTML reports, Excel data (added in later stages) |
Step 1 — Project Structure
shop-automation/
├── pom.xml
└── src/test/
├── java/com/shop/
│ ├── base/ BaseTest.java, BasePage.java, DriverFactory.java
│ ├── config/ ConfigReader.java
│ ├── pages/ LoginPage, ProductsPage, CartPage, CheckoutInfoPage,
│ │ CheckoutOverviewPage, OrderCompletePage
│ └── tests/ LoginTest.java, CheckoutTest.java
└── resources/
├── config/qa.properties
└── testng.xml
All framework code lives under src/test because it's test code — TestNG and Selenium are usually test-scoped dependencies, and code in src/main can't see them.
Step 2 — Configuration
# src/test/resources/config/qa.properties
baseUrl=https://www.saucedemo.com/
browser=chrome
headless=false
timeoutSeconds=10
username=standard_user
public final class ConfigReader {
private static final Properties PROPS = new Properties();
static {
String env = System.getProperty("env", "qa");
try (InputStream in = ConfigReader.class.getResourceAsStream("/config/" + env + ".properties")) {
if (in == null) throw new IllegalStateException("Missing config for env: " + env);
PROPS.load(in);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static String get(String key) {
return System.getProperty(key, PROPS.getProperty(key));
}
}
Values can be overridden from the command line (mvn test -Dbrowser=firefox -Denv=staging). Passwords stay out of the file — they come from an environment variable or Jenkins credentials.
Step 3 — Driver Factory and BaseTest
public final class DriverFactory {
private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
public static void init() {
boolean headless = Boolean.parseBoolean(ConfigReader.get("headless"));
WebDriver driver;
if ("firefox".equalsIgnoreCase(ConfigReader.get("browser"))) {
FirefoxOptions o = new FirefoxOptions();
if (headless) o.addArguments("-headless");
driver = new FirefoxDriver(o);
} else {
ChromeOptions o = new ChromeOptions();
if (headless) o.addArguments("--headless=new", "--window-size=1920,1080");
driver = new ChromeDriver(o);
}
DRIVER.set(driver);
}
public static WebDriver get() {
return DRIVER.get();
}
public static void quit() {
if (DRIVER.get() != null) {
DRIVER.get().quit();
DRIVER.remove();
}
}
}
public abstract class BaseTest {
@BeforeMethod(alwaysRun = true)
public void setUp() {
DriverFactory.init();
DriverFactory.get().get(ConfigReader.get("baseUrl"));
}
@AfterMethod(alwaysRun = true)
public void tearDown() {
DriverFactory.quit();
}
}
The ThreadLocal driver makes parallel runs safe from day one. A fresh browser per test keeps tests independent.
Step 4 — BasePage With Wait Helpers
public abstract class BasePage {
protected final WebDriver driver = DriverFactory.get();
private final WebDriverWait wait = new WebDriverWait(
driver,
Duration.ofSeconds(Long.parseLong(ConfigReader.get("timeoutSeconds")))
);
protected void click(By by) {
wait.until(ExpectedConditions.elementToBeClickable(by)).click();
}
protected void type(By by, String text) {
WebElement e = visible(by);
e.clear();
e.sendKeys(text);
}
protected String text(By by) {
return visible(by).getText().trim();
}
protected WebElement visible(By by) {
return wait.until(ExpectedConditions.visibilityOfElementLocated(by));
}
protected List<WebElement> all(By by) {
return wait.until(ExpectedConditions.visibilityOfAllElementsLocatedBy(by));
}
protected static double money(String label) {
// "Item total: $39.98" → 39.98
return Double.parseDouble(label.replaceAll("[^0-9.]", ""));
}
}
Every page inherits these helpers, so no page ever calls Thread.sleep or findElement without a wait.
Step 5 — Page Objects for the Whole Journey
public class LoginPage extends BasePage {
private final By username = By.id("user-name");
private final By password = By.id("password");
private final By loginBtn = By.id("login-button");
private final By error = By.cssSelector("[data-test='error']");
public ProductsPage loginAs(String user, String pass) {
type(username, user);
type(password, pass);
click(loginBtn);
return new ProductsPage();
}
public String errorMessage() {
return text(error);
}
}
public class ProductsPage extends BasePage {
private final By title = By.cssSelector(".title");
private final By cartLink = By.cssSelector(".shopping_cart_link");
private final By cartBadge = By.cssSelector(".shopping_cart_badge");
private final By sort = By.cssSelector(".product_sort_container");
private final By prices = By.cssSelector(".inventory_item_price");
public String title() {
return text(title);
}
public ProductsPage addToCart(String productName) {
// "Sauce Labs Backpack" → add-to-cart-sauce-labs-backpack
click(By.id("add-to-cart-" + productName.toLowerCase().replace(' ', '-')));
return this;
}
public int cartCount() {
return Integer.parseInt(text(cartBadge));
}
public ProductsPage sortBy(String value) {
// "lohi", "hilo", "az", "za"
new Select(visible(sort)).selectByValue(value);
return this;
}
public List<Double> prices() {
return all(prices)
.stream()
.map(e -> money(e.getText()))
.toList();
}
public CartPage openCart() {
click(cartLink);
return new CartPage();
}
}
public class CartPage extends BasePage {
private final By itemNames = By.cssSelector(".inventory_item_name");
private final By checkout = By.id("checkout");
public List<String> items() {
return all(itemNames)
.stream()
.map(WebElement::getText)
.toList();
}
public CheckoutInfoPage checkout() {
click(checkout);
return new CheckoutInfoPage();
}
}
public class CheckoutInfoPage extends BasePage {
public CheckoutOverviewPage enter(String first, String last, String zip) {
type(By.id("first-name"), first);
type(By.id("last-name"), last);
type(By.id("postal-code"), zip);
click(By.id("continue"));
return new CheckoutOverviewPage();
}
}
public class CheckoutOverviewPage extends BasePage {
public List<Double> itemPrices() {
return all(By.cssSelector(".inventory_item_price"))
.stream()
.map(e -> money(e.getText()))
.toList();
}
public double itemTotal() {
return money(text(By.cssSelector(".summary_subtotal_label")));
}
public double tax() {
return money(text(By.cssSelector(".summary_tax_label")));
}
public double total() {
return money(text(By.cssSelector(".summary_total_label")));
}
public OrderCompletePage finish() {
click(By.id("finish"));
return new OrderCompletePage();
}
}
public class OrderCompletePage extends BasePage {
public String header() {
return text(By.cssSelector(".complete-header"));
}
}
Each action returns the page the user lands on, so a test reads as one fluent journey. Locators stay private; assertions stay in the tests. Why this structure works: Page Object Model and OOP in Your Framework.
Step 6 — The Tests
public class CheckoutTest extends BaseTest {
@Test(groups = {"smoke", "regression"})
public void customerCanBuyTwoProducts() {
ProductsPage products = new LoginPage()
.loginAs(
ConfigReader.get("username"),
System.getenv("APP_PASSWORD")
);
Assert.assertEquals(products.title(), "Products");
products
.addToCart("Sauce Labs Backpack")
.addToCart("Sauce Labs Bike Light");
Assert.assertEquals(products.cartCount(), 2);
CartPage cart = products.openCart();
Assert.assertEquals(
cart.items(),
List.of("Sauce Labs Backpack", "Sauce Labs Bike Light")
);
CheckoutOverviewPage overview =
cart.checkout().enter("Asha", "Rao", "560001");
double sum = overview.itemPrices()
.stream()
.mapToDouble(Double::doubleValue)
.sum();
Assert.assertEquals(
overview.itemTotal(),
sum,
0.001,
"item total = sum of item prices"
);
Assert.assertEquals(
overview.total(),
overview.itemTotal() + overview.tax(),
0.001,
"total = item total + tax"
);
Assert.assertEquals(
overview.finish().header(),
"Thank you for your order!"
);
}
@Test(groups = {"regression"})
public void pricesSortLowToHigh() {
List<Double> prices = new LoginPage()
.loginAs(
ConfigReader.get("username"),
System.getenv("APP_PASSWORD")
)
.sortBy("lohi")
.prices();
List<Double> sorted = new ArrayList<>(prices);
Collections.sort(sorted);
Assert.assertEquals(prices, sorted);
}
}
public class LoginTest extends BaseTest {
@Test(groups = {"regression"})
public void lockedOutUserSeesError() {
LoginPage login = new LoginPage();
login.loginAs(
"locked_out_user",
System.getenv("APP_PASSWORD")
);
Assert.assertTrue(
login.errorMessage().contains("locked out")
);
}
}
Two things make these real tests rather than scripts: every test ends with assertions (a test method without @Test or without assertions proves nothing), and the checkout test calculates the expected totals from the page's own item prices instead of hard-coding numbers that break when prices or tax change.
Step 7 — testng.xml and Running
<suite name="Shop Regression" parallel="methods" thread-count="3">
<test name="All">
<groups>
<run>
<include name="regression"/>
</run>
</groups>
<packages>
<package name="com.shop.tests"/>
</packages>
</test>
</suite>
mvn clean test # full suite from testng.xml
mvn test -Dgroups=smoke # smoke only
mvn test -Dbrowser=firefox -Dheadless=true # another browser, headless
Parallel runs work because each test thread has its own driver. Watch each page action travel from your test to the browser in the Selenium WebDriver Visualizer.
Where to Go Next
- Data-driven logins: Excel/JSON rows into a DataProvider — Data-Driven Testing.
- Reports, screenshots, CI: Reporting, Parallel & CI/CD.
- Faster setup: create carts or orders through an API instead of the UI when the UI isn't what you're testing.
How to Explain It in an Interview
"I built a Selenium Java framework on the Page Object Model with TestNG and Maven. Configuration comes from per-environment properties files that can be overridden from the command line. A ThreadLocal driver factory creates a browser per test so we can run in parallel, and a BasePage holds explicit-wait helpers so pages never use sleeps. Page objects cover the whole purchase journey and return the next page, so tests read like the user's flow. The checkout test verifies the cart and calculates the expected totals from the item prices. We run smoke and regression groups from Jenkins with Extent reports and failure screenshots."
More: How to Explain Your Framework.
From Real Projects
My automation work has been in Java — Selenium WebDriver scripts with TestNG in Eclipse — and I use OOP concepts such as classes and objects, method overloading, overriding and constructors in page classes and utilities. Small programs like this one are the building blocks of real framework code: looping through data, comparing expected and actual values, and handling edge cases. On Canolog, the many forms and screens across sales, inventory, finance and service are where keeping page details in POM classes and reusable steps in a business library paid off. Build the framework in the order shown and run it after every step.
Frequently Asked Questions
Which technologies does this framework use?
Java, Selenium WebDriver 4, TestNG and Maven with the Page Object Model — plus Log4j2, Extent Reports and Apache POI as it grows.
Why does the framework code live in src/test?
It's test code, and TestNG/Selenium are test-scoped dependencies that code in src/main can't see.
What does BaseTest do?
Creates a fresh browser before each test, opens the base URL, and quits the browser after — with alwaysRun = true so cleanup happens even when setup fails.
Why return page objects from page methods?
So tests follow the user's journey fluently and the compiler guides you to the right next page.
How is configuration managed?
Per-environment properties files read by a ConfigReader, overridable with -D system properties; secrets come from environment variables.
Why calculate totals in the test instead of hard-coding them?
Hard-coded prices break whenever data changes; calculating from the displayed item prices checks the real rule — that the totals add up.