Data-Driven Testing
In the last lesson, testing RecipeBox's login page meant writing one method for a valid login and a second method for an invalid one. What happens when you need to check four, ten, or twenty different input combinations — empty email, malformed email, wrong password, valid credentials, and so on? You don't write twenty near-identical test methods. You write one test method and feed it many rows of data. That's data-driven testing, and in TestNG it's built on the @DataProvider annotation.
@DataProvider Basics
A data provider is just a method annotated with @DataProvider that returns an Object[][] — an array of rows, where each row is an array of arguments. TestNG runs the linked @Test method once per row, passing that row's values as the method's parameters.
@DataProvider(name = "loginCredentials")
public Object[][] loginCredentials() {
return new Object[][] {
{ "tester@example.com", "Test@1234", true }, // valid
{ "", "Test@1234", false }, // empty email
{ "tester@example.com", "WrongPass", false }, // wrong password
{ "not-an-email", "Test@1234", false } // malformed email
};
}
Link a test method to it by passing the provider's name to @Test's dataProvider attribute:
@Test(dataProvider = "loginCredentials")
public void loginAttempt(String email, String password, boolean expectedSuccess) {
// this method body runs 4 times — once per row above
}
Worked Example: RecipeBox Login With 4 Credential Combinations
One valid login and three invalid variations — empty email, wrong password, and a malformed email — all driven from a single data provider and a single test method.
public class RecipeBoxDataDrivenLoginTest {
private WebDriver driver;
@BeforeMethod
public void setUp() {
driver = new ChromeDriver();
driver.get("https://recipebox.example.com/login");
}
@DataProvider(name = "loginCredentials")
public Object[][] loginCredentials() {
return new Object[][] {
{ "tester@example.com", "Test@1234", true },
{ "", "Test@1234", false },
{ "tester@example.com", "WrongPass", false },
{ "not-an-email", "Test@1234", false }
};
}
@Test(dataProvider = "loginCredentials")
public void loginAttempt(String email, String password, boolean expectedSuccess) {
driver.findElement(By.id("email")).clear();
driver.findElement(By.id("email")).sendKeys(email);
driver.findElement(By.id("password")).clear();
driver.findElement(By.id("password")).sendKeys(password);
driver.findElement(By.id("loginBtn")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
if (expectedSuccess) {
wait.until(ExpectedConditions.urlContains("/recipes"));
Assert.assertTrue(driver.getCurrentUrl().contains("/recipes"),
"Valid credentials should reach the recipe list");
} else {
WebElement error = wait.until(
ExpectedConditions.visibilityOfElementLocated(By.className("login-error"))
);
Assert.assertTrue(error.isDisplayed(),
"Invalid credentials should show a login error");
}
}
@AfterMethod
public void tearDown() {
if (driver != null) driver.quit();
}
}
When to Reach for @DataProvider
| Good fit | Not a good fit |
|---|---|
| Same steps, many input combinations (login, search, form validation) | Fundamentally different flows per case (that's just separate test methods) |
| Boundary/edge-case coverage (empty, too long, special characters) | A single one-off scenario you'll only ever run once |
| Values that could later come from a CSV/Excel file | Anything where the row count is always exactly 1 |
That last row is a preview: once you have login credentials living in a @DataProvider method, it's a short step to sourcing those same rows from an external Excel or CSV file instead of hardcoding them — which we'll cover later in this track.