@Factory Annotation for Dynamic Test Instances

8 min read

@DataProvider and @Factory both produce multiple test executions from a single piece of code, but they operate at different levels. @DataProvider calls the same method on the same object multiple times with different arguments. @Factory creates multiple objects of the same test class, each initialised with different constructor arguments — and then runs every @Test method on every one of those objects. When you need class-level state to vary (different browsers, different base URLs, different configurations per object), @Factory is the right tool. This lesson covers the factory method, combining @Factory with @DataProvider for clean data supply, and the practical use cases where @Factory outperforms its alternatives.

A basic @Factory

package com.mycompany.tests.tests;
 
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.testng.Assert;
import org.testng.annotations.*;
 
public class CrossBrowserLoginTest {
 
    private final String browser;
    private final String baseUrl;
    private WebDriver driver;
 
    // Constructor receives the per-instance config
    public CrossBrowserLoginTest(String browser, String baseUrl) {
        this.browser = browser;
        this.baseUrl = baseUrl;
    }
 
    @BeforeMethod
    public void setup() {
        driver = browser.equalsIgnoreCase("firefox")
            ? new FirefoxDriver()
            : new ChromeDriver();
        driver.manage().window().maximize();
        driver.get(baseUrl);
    }
 
    @Test(description = "Login form is visible on the landing page")
    public void loginFormIsVisible() {
        Assert.assertTrue(
            driver.findElement(org.openqa.selenium.By.id("user-name")).isDisplayed(),
            "Login form missing on " + browser
        );
    }
 
    @Test(description = "Valid credentials navigate to inventory")
    public void validLoginSucceeds() {
        driver.findElement(org.openqa.selenium.By.id("user-name")).sendKeys("standard_user");
        driver.findElement(org.openqa.selenium.By.id("password")).sendKeys("secret_sauce");
        driver.findElement(org.openqa.selenium.By.id("login-button")).click();
        Assert.assertTrue(driver.getCurrentUrl().contains("/inventory.html"),
            "Inventory URL expected on " + browser);
    }
 
    @AfterMethod(alwaysRun = true)
    public void teardown() {
        if (driver != null) driver.quit();
    }
}

The factory class that creates instances of this test class:

package com.mycompany.tests.factory;
 
import com.mycompany.tests.tests.CrossBrowserLoginTest;
import org.testng.annotations.Factory;
 
public class BrowserFactory {
 
    @Factory
    public Object[] createTests() {
        String url = "https://www.saucedemo.com";
        return new Object[] {
            new CrossBrowserLoginTest("chrome",  url),
            new CrossBrowserLoginTest("firefox", url),
            new CrossBrowserLoginTest("edge",    url),
        };
    }
}

Register the factory in testng.xml:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Cross Browser" parallel="instances" thread-count="3">
    <test name="All Browsers">
        <classes>
            <class name="com.mycompany.tests.factory.BrowserFactory"/>
        </classes>
    </test>
</suite>

TestNG creates three CrossBrowserLoginTest instances (chrome, firefox, edge), and on each instance it runs both @Test methods — producing 6 test results: loginFormIsVisible × 3 browsers and validLoginSucceeds × 3 browsers.

@Factory with @DataProvider

Instead of hardcoding the new Object[] array, use a @DataProvider to supply constructor arguments:

package com.mycompany.tests.tests;
 
import org.testng.annotations.DataProvider;
import org.testng.annotations.Factory;
 
public class EnvironmentLoginTest {
 
    private final String browser;
    private final String environment;
    private final String baseUrl;
 
    // @Factory constructor receives args from the DataProvider
    @Factory(dataProvider = "browserEnvData")
    public EnvironmentLoginTest(String browser, String environment, String baseUrl) {
        this.browser     = browser;
        this.environment = environment;
        this.baseUrl     = baseUrl;
    }
 
    @DataProvider(name = "browserEnvData")
    public static Object[][] browserEnvData() {
        return new Object[][] {
            {"chrome",  "staging",    "https://staging.myapp.com"},
            {"firefox", "staging",    "https://staging.myapp.com"},
            {"chrome",  "production", "https://myapp.com"},
        };
    }
 
    @org.testng.annotations.Test
    public void loginFormRendersCorrectly() {
        System.out.printf("[%s / %s] Testing login form at %s%n",
            browser, environment, baseUrl);
        // driver setup and assertions
    }
}

The @DataProvider is static and lives in the same class as @Factory. TestNG calls it, creates one instance per row, and runs loginFormRendersCorrectly on each. External JSON or CSV can feed the data provider — change the matrix without touching Java.

@Factory vs @DataProvider — choosing correctly

@DataProvider vs @Factory — what each creates

@DataProvider

  • ONE instance of the test class

  • Same method called N times

  • Different method arguments each call

  • Class-level state (@BeforeClass) runs once

  • Ideal for testing one method with varied inputs

  • Use for: login with 10 credential sets

@Factory

  • N INSTANCES of the test class

  • All @Test methods run on each instance

  • Each instance has different constructor state

  • Class-level state (@BeforeClass) runs per instance

  • Ideal for config-variant test suites

  • Use for: same tests across 3 browsers

Parallel @Factory execution

With parallel="instances" in testng.xml, each factory-created instance runs on its own thread:

<suite name="Cross Browser" parallel="instances" thread-count="3">

Three instances run concurrently — one per browser. Each instance has its own browser, baseUrl, and eventually its own driver instance, so there's no shared mutable state. This is the safest form of TestNG parallelism: isolation is enforced at the object boundary, not just via ThreadLocal.

For Selenium parallel execution, @Factory + parallel="instances" is cleaner than parallel="methods" + ThreadLocal<WebDriver>: the design makes isolation explicit rather than relying on thread-local discipline.

When to use @Factory

Three cases where @Factory is the natural fit:

  1. Cross-browser runs. Each browser gets its own CrossBrowserLoginTest instance with a different driver. All tests run on every browser.
  2. Multi-environment runs. One factory creates instances for staging, UAT, and production — same tests, different baseUrl per instance.
  3. Multi-tenant applications. Each tenant has different configuration. Create one test class per tenant via @Factory rather than writing a separate class.

Avoid @Factory when you only need to vary method-level test data — that's @DataProvider's job. A factory that creates 20 instances to feed different inputs to one method is the right tool used wrong.

⚠️ Common mistakes

  • Non-static @DataProvider used with @Factory. The factory constructor runs during TestNG's discovery phase before any instance is fully available. TestNG requires the @DataProvider that feeds a @Factory constructor to be static. Non-static providers throw NullPointerException or TestNGException: cannot find data provider at suite startup.
  • Forgetting alwaysRun = true on teardown when using @Factory + parallel="instances". In parallel execution, a setup failure on one instance should not leak resources. @AfterMethod(alwaysRun = true) ensures teardown runs on every thread even when setup threw — same discipline as single-threaded tests, just more important to enforce.
  • Using @Factory when @DataProvider is sufficient. If the only difference between instances is method-level input, @DataProvider is simpler — one instance, same lifecycle, less overhead. @Factory shines when you need different class-level state. Ask: "Does the setup logic change per variant?" If yes, @Factory. If only the test method arguments change, @DataProvider.

🎯 Practice task

Build a cross-browser factory. 30–40 minutes.

  1. Create CrossBrowserLoginTest with a constructor taking browser and baseUrl. Implement @BeforeMethod that creates the right driver based on browser, and @AfterMethod(alwaysRun = true) that quits it.
  2. Write BrowserFactory returning three instances (chrome, firefox — skip edge if you only have two browsers installed). Register it in a cross-browser.xml with parallel="instances" thread-count="2".
  3. Run. Confirm the console shows two browsers starting in parallel and each running both @Test methods. Total results: 4 (2 tests × 2 browsers).
  4. Switch to @Factory(dataProvider = ...) in EnvironmentLoginTest. Move the browser/URL data to an external JSON file and load it via DataReader.fromJson. Add a third row for a third browser. Run — the factory creates the third instance automatically.
  5. Observe @BeforeClass behaviour. Add a @BeforeClass that prints [CLASS SETUP: " + browser + "]. Run. Confirm it fires once per instance — three times for three instances — not once per suite. This is the key difference from a regular single-instance test class.
  6. Stretch — environment × browser matrix. Create a factory with 6 instances: 2 browsers × 3 environments. Confirm all 6 instances appear in the report. Open test-output/index.html and observe how TestNG names the instances.

Next chapter: advanced TestNG. Parallel execution modes, listeners for screenshots and custom reports, retry logic for flaky tests, and annotation transformers.

// tip to track lessons you complete and pick up where you left off across devices.