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.