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

TypeWhat It DoesAutomation Use Case
Simple FactoryOne static method picks the right classWebDriver Factory — give me Chrome or Firefox
Factory MethodSubclasses decide which object to createBase test class with customizable driver creation
Abstract FactoryFactory of factories — creates families of objectsCreate 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

ComponentFactory?Reason
WebDriver creationYesMultiple browsers, options, headless mode
Test dataYesMultiple user types, dynamic data, reuse
API clientsYesMultiple services, auth headers, base URLs
Page ObjectsMaybeUseful with 20+ pages, optional with DI
AssertionsNoUse assertion libraries directly
String buildersNoTrivial creation, no variants

Summary