Parameterised Tests with @DataProvider

7 min read

Data-driven tests run the same logic with different inputs and expected outputs. TestNG's @DataProvider is the native mechanism for parameterising test methods — no external library required.

Basic @DataProvider

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
 
public class LoginTest extends BaseTest {
 
    @DataProvider(name = "loginCredentials")
    public Object[][] provideCredentials() {
        return new Object[][] {
            { "valid@example.com",   "password123", true,  null },
            { "wrong@example.com",   "password123", false, "Email not found" },
            { "valid@example.com",   "wrongpass",   false, "Incorrect password" },
            { "notanemail",          "password123", false, "Invalid email format" },
            { "",                    "",            false, "Email is required" },
        };
    }
 
    @Test(dataProvider = "loginCredentials")
    public void testLogin(String email, String password, boolean expectSuccess, String expectedError) {
        LoginPage loginPage = new LoginPage(getDriver());
        loginPage.enterCredentials(email, password);
        loginPage.tapLogin();
 
        if (expectSuccess) {
            assertThat(new HomePage(getDriver()).isDisplayed()).isTrue();
        } else {
            assertThat(loginPage.getErrorMessage()).isEqualTo(expectedError);
        }
    }
}

TestNG runs testLogin once per row, substituting each row's values into the method parameters. Failed rows are reported individually — a failure on row 3 doesn't block rows 4 and 5.

Returning named objects instead of raw arrays

Object[][] loses type information and gets messy with many columns. Define a data class:

public record LoginTestData(
    String email,
    String password,
    boolean expectSuccess,
    String expectedError
) {}
 
@DataProvider(name = "loginCredentials")
public Object[][] provideCredentials() {
    return new Object[][] {
        { new LoginTestData("valid@example.com", "pass123", true, null) },
        { new LoginTestData("",                  "",        false, "Email is required") },
    };
}
 
@Test(dataProvider = "loginCredentials")
public void testLogin(LoginTestData data) {
    LoginPage loginPage = new LoginPage(getDriver());
    loginPage.enterCredentials(data.email(), data.password());
    loginPage.tapLogin();
    // ...
}

The test method receives a single typed object per row.

Reading data from external files

For large data sets, read from CSV or JSON rather than hardcoding in the provider method:

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.util.List;
 
@DataProvider(name = "productsFromJson")
public Object[][] readProductData() throws Exception {
    ObjectMapper mapper = new ObjectMapper();
    List<Map<String, Object>> products = mapper.readValue(
        new File("src/test/resources/testdata/products.json"),
        new TypeReference<>() {}
    );
 
    return products.stream()
        .map(p -> new Object[] { p })
        .toArray(Object[][]::new);
}

src/test/resources/testdata/products.json:

[
  { "name": "Wireless Headphones", "expectedPrice": "$99.99", "inStock": true },
  { "name": "USB-C Hub",           "expectedPrice": "$49.99", "inStock": true },
  { "name": "Discontinued Item",   "expectedPrice": null,     "inStock": false }
]

Parallel data providers

By default, TestNG runs data provider iterations sequentially. Add parallel = true to run them concurrently:

@DataProvider(name = "loginCredentials", parallel = true)
public Object[][] provideCredentials() {
    return new Object[][] { ... };
}

Parallel data providers require the test class to be thread-safe — the driver must come from DriverManager.getDriver(), not from a shared field. Each parallel iteration runs in its own thread.

Control the thread count in testng.xml:

<suite name="Suite" data-provider-thread-count="4">

DataProvider in a separate class

As suites grow, data providers accumulate. Move them to dedicated classes:

public class TestDataProviders {
 
    @DataProvider(name = "checkoutData")
    public Object[][] checkoutData() { ... }
 
    @DataProvider(name = "searchTerms")
    public Object[][] searchTerms() { ... }
}
 
// In the test class:
@Test(dataProvider = "checkoutData", dataProviderClass = TestDataProviders.class)
public void testCheckout(CheckoutData data) { ... }

dataProviderClass tells TestNG where to find the method — it can be any class, not just the test class.

Naming iterations for reports

By default, TestNG reports data provider iterations as testLogin[0], testLogin[1]. Override toString() on your data class to get meaningful names:

public record LoginTestData(String email, String password, boolean expectSuccess, String expectedError) {
    @Override
    public String toString() {
        return (expectSuccess ? "valid login" : "invalid login") + " [" + email + "]";
    }
}

Allure and ExtentReports pick up the parameter's toString() for iteration labels.

Combining DataProvider with platform parameterisation

In mobile suites with cross-platform tests, you may need data-driven tests on both platforms:

@Test(dataProvider = "loginCredentials")
@Parameters("platform")
public void testLogin(String email, String password, boolean expectSuccess, String expectedError) {
    // @Parameters injects "platform" from testng.xml
    // @DataProvider injects the row values
    // Both work together
}

TestNG injects @Parameters values after the DataProvider values, so parameter order is: DataProvider columns first, then @Parameters values.

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