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
}
Note: The parameters of the test method must line up, in order and type, with the values in each row of the Object[][]. TestNG reports each row as a separate test result (e.g. loginAttempt[0], loginAttempt[1]...), so one bad row failing doesn't hide the others passing.

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();
    }
}
Tip: Because @BeforeMethod/@AfterMethod run around every invocation of a data-provided test, each of the four rows above gets its own fresh browser session — row 2's empty-email attempt can never accidentally see leftover state from row 1's valid login.

When to Reach for @DataProvider

Good fitNot 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 fileAnything 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.

Next up: Your test classes are calling driver.findElement(By.id("email")) directly — fine for a handful of tests, painful once the login form's locators are duplicated across twenty files. Head to Page Object Model (POM) to fix that.