Factory Pattern
WebDriver Factory, Page Factory, Test Data Factory
What is the Factory Pattern?
The Factory Pattern creates objects without exposing the creation logic to the caller. Instead of using new directly, you ask a factory to give you the right object based on a parameter.
Why It Matters in Test Automation
In test automation frameworks, you constantly create:
- WebDriver instances — Chrome, Firefox, Edge, headless
- Page Objects — LoginPage, DashboardPage, CartPage
- Test Data — valid users, invalid users, admin users
- API Clients — REST, GraphQL, SOAP
Without Factory Pattern, your test code is littered with if/else blocks and new calls. With it, creation logic lives in one place — change once, update everywhere.
Three Flavors
| Type | What It Does | Automation Use Case |
|---|---|---|
| Simple Factory | One static method picks the right class | WebDriver Factory — give me Chrome or Firefox |
| Factory Method | Subclasses decide which object to create | Base test class with customizable driver creation |
| Abstract Factory | Factory of factories — creates families of objects | Create entire env setup (driver + config + data) per environment |
WebDriver Factory
The #1 most common use of Factory Pattern in Selenium automation.
Without Factory (The Problem)
// This is scattered across every test or hook class
public class LoginTest {
WebDriver driver;
@Before
public void setup() {
String browser = System.getProperty("browser", "chrome");
if (browser.equals("chrome")) {
ChromeOptions options = new ChromeOptions();
options.addArguments("--disable-gpu");
if (Boolean.parseBoolean(System.getProperty("headless"))) {
options.addArguments("--headless=new");
}
driver = new ChromeDriver(options);
} else if (browser.equals("firefox")) {
FirefoxOptions options = new FirefoxOptions();
if (Boolean.parseBoolean(System.getProperty("headless"))) {
options.addArguments("--headless");
}
driver = new FirefoxDriver(options);
} else if (browser.equals("edge")) {
driver = new EdgeDriver();
}
driver.manage().window().maximize();
}
}
// Problem: This same 20-line block is copy-pasted in every test class!
With Factory (The Solution)
public class DriverFactory {
public static WebDriver createDriver(String browser) {
WebDriver driver;
boolean headless = Boolean.parseBoolean(
System.getProperty("headless", "false")
);
switch (browser.toLowerCase()) {
case "firefox" -> {
FirefoxOptions options = new FirefoxOptions();
if (headless) options.addArguments("--headless");
driver = new FirefoxDriver(options);
}
case "edge" -> {
EdgeOptions options = new EdgeOptions();
if (headless) options.addArguments("--headless=new");
driver = new EdgeDriver(options);
}
default -> {
ChromeOptions options = new ChromeOptions();
options.addArguments("--disable-gpu", "--no-sandbox");
if (headless) options.addArguments("--headless=new");
driver = new ChromeDriver(options);
}
}
driver.manage().window().maximize();
driver.manage().timeouts()
.implicitlyWait(Duration.ofSeconds(10));
return driver;
}
// Overloaded — uses config property if no browser specified
public static WebDriver createDriver() {
String browser = System.getProperty("browser", "chrome");
return createDriver(browser);
}
}
Usage — Clean and Simple
// In Cucumber Hooks
@Before("@ui")
public void setUp() {
WebDriver driver = DriverFactory.createDriver(); // One line!
context.setDriver(driver);
}
// In TestNG test
@BeforeMethod
public void setUp() {
driver = DriverFactory.createDriver("firefox");
}
// From command line
// mvn test -Dbrowser=edge -Dheadless=true
Enhanced Factory with Enum
For a more robust, type-safe approach, use an enum-based factory:
public enum BrowserType {
CHROME {
@Override
public WebDriver create(boolean headless) {
ChromeOptions options = new ChromeOptions();
options.addArguments("--disable-gpu", "--no-sandbox",
"--window-size=1920,1080");
if (headless) options.addArguments("--headless=new");
return new ChromeDriver(options);
}
},
FIREFOX {
@Override
public WebDriver create(boolean headless) {
FirefoxOptions options = new FirefoxOptions();
if (headless) options.addArguments("--headless");
return new FirefoxDriver(options);
}
},
EDGE {
@Override
public WebDriver create(boolean headless) {
EdgeOptions options = new EdgeOptions();
if (headless) options.addArguments("--headless=new");
return new EdgeDriver(options);
}
};
public abstract WebDriver create(boolean headless);
}
// Factory class
public class DriverFactory {
public static WebDriver createDriver() {
String browserName = System.getProperty("browser", "chrome");
boolean headless = Boolean.parseBoolean(
System.getProperty("headless", "false")
);
BrowserType browser = BrowserType.valueOf(
browserName.toUpperCase()
);
WebDriver driver = browser.create(headless);
driver.manage().window().maximize();
driver.manage().timeouts()
.implicitlyWait(Duration.ofSeconds(10));
return driver;
}
}
Page Object Factory
When your framework has 30+ page objects, creating them manually in every step definition gets repetitive. A Page Factory centralizes this:
Without Factory
// Scattered across step definitions
LoginPage loginPage = new LoginPage(driver);
DashboardPage dashPage = new DashboardPage(driver);
CartPage cartPage = new CartPage(driver);
CheckoutPage checkoutPage = new CheckoutPage(driver);
// Repeated everywhere...
With Factory
public class PageFactory {
private final WebDriver driver;
private final Map<Class<?>, Object> pageCache = new HashMap<>();
public PageFactory(WebDriver driver) {
this.driver = driver;
}
@SuppressWarnings("unchecked")
public <T extends BasePage> T getPage(Class<T> pageClass) {
// Cache pages to avoid re-creating them
return (T) pageCache.computeIfAbsent(pageClass, cls -> {
try {
return cls.getConstructor(WebDriver.class)
.newInstance(driver);
} catch (Exception e) {
throw new RuntimeException(
"Cannot create page: " + cls.getSimpleName(), e);
}
});
}
}
// Usage in step definitions
public class LoginSteps {
private final PageFactory pages;
public LoginSteps(TestContext context) {
this.pages = new PageFactory(context.getDriver());
}
@Given("I am on the login page")
public void navigateToLogin() {
pages.getPage(LoginPage.class).navigate();
}
@When("I login with {string} and {string}")
public void login(String user, String pass) {
pages.getPage(LoginPage.class).login(user, pass);
}
@Then("I should see the dashboard")
public void verifyDashboard() {
Assert.assertTrue(
pages.getPage(DashboardPage.class).isDisplayed()
);
}
}
Test Data Factory
Hard-coded test data is one of the biggest maintenance problems. A Test Data Factory creates realistic, reusable test objects:
Without Factory
// Hard-coded everywhere — breaks when data requirements change
loginPage.login("admin", "Admin@123");
registrationPage.fillForm("John", "Doe", "john@test.com", "Pass123!");
// 50 tests with the same hard-coded strings...
With Factory
public class TestDataFactory {
// ── User Data ─────────────────────────────────
public static User validAdmin() {
return new User("admin", "Admin@123", "ADMIN");
}
public static User validCustomer() {
return new User("customer1", "Cust@456", "CUSTOMER");
}
public static User invalidUser() {
return new User("nonexistent", "wrong", "NONE");
}
public static User lockedUser() {
return new User("locked_user", "Locked@789", "CUSTOMER");
}
// ── Dynamic Data (unique per test run) ────────
public static User randomUser() {
String unique = UUID.randomUUID().toString().substring(0, 8);
return new User(
"user_" + unique,
"Pass@" + unique,
"CUSTOMER"
);
}
// ── Product Data ──────────────────────────────
public static Product sampleProduct() {
return new Product("Laptop Pro", 999.99, "Electronics");
}
public static List<Product> multipleProducts(int count) {
return IntStream.rangeClosed(1, count)
.mapToObj(i -> new Product("Product " + i, 10.0 * i, "General"))
.collect(Collectors.toList());
}
// ── Registration Data ─────────────────────────
public static Registration validRegistration() {
String unique = UUID.randomUUID().toString().substring(0, 6);
return new Registration(
"John", "Doe",
"john." + unique + "@test.com",
"1234567890",
"SecurePass@123"
);
}
}
Usage in Tests
// Clean, readable, maintainable
@When("I login as admin")
public void loginAsAdmin() {
User admin = TestDataFactory.validAdmin();
loginPage.login(admin.getUsername(), admin.getPassword());
}
@When("I register a new account")
public void registerNewAccount() {
Registration reg = TestDataFactory.validRegistration();
registrationPage.fillForm(reg);
}
// Data changes? Update one factory method, all tests use it.
API Client Factory
When your framework tests multiple APIs or environments:
public class ApiClientFactory {
public static RequestSpecification createClient(String apiType) {
switch (apiType.toLowerCase()) {
case "user-service" -> {
return RestAssured.given()
.baseUri(ConfigReader.get("user.api.baseUrl"))
.contentType(ContentType.JSON)
.header("Authorization",
"Bearer " + TokenManager.getToken());
}
case "payment-service" -> {
return RestAssured.given()
.baseUri(ConfigReader.get("payment.api.baseUrl"))
.contentType(ContentType.JSON)
.header("X-API-Key",
ConfigReader.get("payment.api.key"));
}
case "public-api" -> {
return RestAssured.given()
.baseUri(ConfigReader.get("public.api.baseUrl"))
.contentType(ContentType.JSON);
}
default -> throw new IllegalArgumentException(
"Unknown API: " + apiType
);
}
}
}
// Usage
@When("I fetch user details via API")
public void fetchUser() {
response = ApiClientFactory.createClient("user-service")
.pathParam("id", userId)
.get("/users/{id}");
}
@When("I process a payment via API")
public void processPayment() {
response = ApiClientFactory.createClient("payment-service")
.body(paymentPayload)
.post("/payments");
}
Factory Method — Subclass Decides
Factory Method lets subclasses override the creation logic. Useful when you need different setups for different test suites:
// Abstract base test with factory method
public abstract class BaseTest {
protected WebDriver driver;
@BeforeMethod
public void setUp() {
driver = createDriver(); // Subclass decides HOW to create
driver.get(getBaseUrl()); // Subclass decides WHERE to navigate
}
// Factory methods — subclasses override
protected abstract WebDriver createDriver();
protected abstract String getBaseUrl();
@AfterMethod
public void tearDown() {
if (driver != null) driver.quit();
}
}
// Chrome test suite
public class ChromeSmokeTests extends BaseTest {
@Override
protected WebDriver createDriver() {
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
return new ChromeDriver(options);
}
@Override
protected String getBaseUrl() {
return "https://staging.example.com";
}
@Test
public void testLogin() { /* test code */ }
}
// Firefox regression suite — different driver, different env
public class FirefoxRegressionTests extends BaseTest {
@Override
protected WebDriver createDriver() {
return new FirefoxDriver();
}
@Override
protected String getBaseUrl() {
return "https://qa.example.com";
}
@Test
public void testCheckout() { /* test code */ }
}
When to Use & When Not To
Factory Pattern in Your Framework — Checklist
| Component | Factory? | Reason |
|---|---|---|
| WebDriver creation | Yes | Multiple browsers, options, headless mode |
| Test data | Yes | Multiple user types, dynamic data, reuse |
| API clients | Yes | Multiple services, auth headers, base URLs |
| Page Objects | Maybe | Useful with 20+ pages, optional with DI |
| Assertions | No | Use assertion libraries directly |
| String builders | No | Trivial creation, no variants |