Reading Test Data from Excel/CSV

Back in the @DataProvider lesson, our test data lived inside the Java file as a hard-coded Object[][]. That's fine for 3 rows. It falls apart the moment a QA lead hands you a spreadsheet of 20 RecipeBox test recipes — titles, categories, and prep times — to bulk-test the "Add Recipe" form, and expects to edit that spreadsheet directly without touching Java code. This lesson reads that spreadsheet at runtime instead.

RecipeTestData.xlsx — first few rows of our 20-row sheet:

TitleCategoryPrepTimeMinutes
Paneer Butter MasalaMain Course35
Masala DosaBreakfast25
Chocolate Lava CakeDessert20
Veg Hakka NoodlesMain Course15
………
(16 more rows, 20 total)

Option 1: Apache POI (.xlsx)

Apache POI is the standard Java library for reading and writing Microsoft Office files. Add it to your Maven pom.xml:

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version>
</dependency>

Helper: Reading Rows into Object[][]

This is the exact shape a TestNG @DataProvider needs — an Object[][] where each inner array is one test's arguments:

public class ExcelUtils {

    public static Object[][] readRecipeData(String filePath, String sheetName) throws IOException {
        FileInputStream fis = new FileInputStream(filePath);
        Workbook workbook = new XSSFWorkbook(fis);
        Sheet sheet = workbook.getSheet(sheetName);

        // Row 0 is the header, so data rows run 1..lastRowNum
        int rowCount = sheet.getLastRowNum();
        Object[][] data = new Object[rowCount][3];

        for (int i = 1; i <= rowCount; i++) {
            Row row = sheet.getRow(i);
            data[i - 1][0] = row.getCell(0).getStringCellValue();               // Title
            data[i - 1][1] = row.getCell(1).getStringCellValue();               // Category
            data[i - 1][2] = (int) row.getCell(2).getNumericCellValue();  // PrepTimeMinutes
        }

        workbook.close();
        fis.close();
        return data;
    }
}

Feeding It Into @DataProvider

public class AddRecipeTest {

    @DataProvider(name = "recipeData")
    public Object[][] getRecipeData() throws IOException {
        return ExcelUtils.readRecipeData("testdata/RecipeTestData.xlsx", "Sheet1");
    }

    @Test(dataProvider = "recipeData")
    public void testAddRecipe(String title, String category, int prepTime) {
        AddRecipePage addRecipePage = new AddRecipePage(driver);
        addRecipePage.enterTitle(title);
        addRecipePage.selectCategory(category);
        addRecipePage.enterPrepTime(prepTime);
        addRecipePage.save();

        Assert.assertTrue(addRecipePage.isRecipeSaved(title),
            "Recipe '" + title + "' did not save successfully");
    }
}
Result: TestNG runs testAddRecipe 20 times, once per spreadsheet row, without a single hard-coded value in the test class. Add a 21st recipe to the spreadsheet and the suite picks it up automatically next run.

Option 2: Plain CSV (No Extra Dependency)

Not every team wants the POI dependency (it's fairly heavy) just to read some rows. If your data doesn't need Excel formulas, multiple sheets, or formatting, a plain .csv file read with BufferedReader does the same job with zero extra libraries:

RecipeTestData.csv:

Title,Category,PrepTimeMinutes
Paneer Butter Masala,Main Course,35
Masala Dosa,Breakfast,25
Chocolate Lava Cake,Dessert,20
Veg Hakka Noodles,Main Course,15
public class CsvUtils {

    public static Object[][] readRecipeCsv(String filePath) throws IOException {
        List<String[]> rows = new ArrayList<>();
        BufferedReader reader = new BufferedReader(new FileReader(filePath));

        String line;
        boolean isHeader = true;
        while ((line = reader.readLine()) != null) {
            if (isHeader) { isHeader = false; continue; } // skip header row
            rows.add(line.split(","));
        }
        reader.close();

        Object[][] data = new Object[rows.size()][3];
        for (int i = 0; i < rows.size(); i++) {
            String[] cols = rows.get(i);
            data[i][0] = cols[0];
            data[i][1] = cols[1];
            data[i][2] = Integer.parseInt(cols[2]);
        }
        return data;
    }
}
Which one should you use? If your test data is already maintained in Excel by a non-technical teammate, or has multiple sheets/columns, use POI. If it's simple rows you'll edit in a text editor or generate from another tool, plain CSV keeps your pom.xml leaner. Both plug into @DataProvider the exact same way.
Watch out: line.split(",") breaks if a field itself contains a comma (e.g. a recipe title like "Rice, Fried Style"). For anything beyond the simplest CSVs, a small library like commons-csv handles quoting correctly — but for a flat list of recipe titles, the naive split is usually good enough.
Next: now that the suite has 20 data-driven test cases, running them one after another gets slow. Head to Selenium Grid & Parallel Execution to run them concurrently instead.