@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:
- Cross-browser runs. Each browser gets its own
CrossBrowserLoginTestinstance with a differentdriver. All tests run on every browser. - Multi-environment runs. One factory creates instances for staging, UAT, and production — same tests, different
baseUrlper instance. - Multi-tenant applications. Each tenant has different configuration. Create one test class per tenant via
@Factoryrather 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
@DataProviderused with@Factory. The factory constructor runs during TestNG's discovery phase before any instance is fully available. TestNG requires the@DataProviderthat feeds a@Factoryconstructor to bestatic. Non-static providers throwNullPointerExceptionorTestNGException: cannot find data providerat suite startup. - Forgetting
alwaysRun = trueon 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
@Factorywhen@DataProvideris sufficient. If the only difference between instances is method-level input,@DataProvideris simpler — one instance, same lifecycle, less overhead.@Factoryshines 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.
- Create
CrossBrowserLoginTestwith a constructor takingbrowserandbaseUrl. Implement@BeforeMethodthat creates the right driver based onbrowser, and@AfterMethod(alwaysRun = true)that quits it. - Write
BrowserFactoryreturning three instances (chrome, firefox — skip edge if you only have two browsers installed). Register it in across-browser.xmlwithparallel="instances" thread-count="2". - Run. Confirm the console shows two browsers starting in parallel and each running both
@Testmethods. Total results: 4 (2 tests × 2 browsers). - Switch to
@Factory(dataProvider = ...)inEnvironmentLoginTest. Move the browser/URL data to an external JSON file and load it viaDataReader.fromJson. Add a third row for a third browser. Run — the factory creates the third instance automatically. - Observe
@BeforeClassbehaviour. Add a@BeforeClassthat 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. - 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.htmland 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.