Optimize Your Automation Framework: Using Data-Driven Testing and Screenshots (Part 4)


Welcome back to the fourth installment of our Selenium Framework Design! So far we have built a standalone Selenium test for an e-commerce application, adopted the Page Object Model (POM), introduced a BaseTest class, TestNG for test management, and advanced features like parallel execution and dependent tests. Now, in Part 4, we’ll dive into data-driven testing using TestNG’s @DataProvider
, learn how to drive test data from JSON files, and create a screenshot utility for failed tests. This guide, will walk you through every step to make your Selenium framework more powerful and intelligent. Let’s get started!
Recap: Where We Are
In Part 3, we:
Created a
BaseTest
class to centralize WebDriver setup and browser configuration.Used a
GlobalData.properties
file to dynamically select browsers (Chrome, Firefox, Edge).Implemented TestNG annotations (
@BeforeMethod
,@AfterMethod
) to streamline setup and teardown.Organized tests into logical groups (e.g.,
ErrorValidationsTest
,SubmitOrderTest
) and ran them viatestng.xml
.Enabled parallel execution and selective test runs using TestNG groups.
Added dependent tests to verify order history after order submission.
Now, we’ll focus on parameterization to run tests with multiple data sets, use JSON as an external data source, and build a screenshot utility to capture failures for better debugging.
Step 1: Understanding Data-Driven Testing with TestNG DataProvider
Data-driven testing allows a single test to run multiple times with different input data, reducing code duplication. TestNG’s @DataProvider
annotation makes this easy by supplying multiple data sets to a test method. If you’re new to @DataProvider
, don’t worry we’ll go through it step-by-step. (For a deeper dive, you can check the TestNG Guide)
Creating a DataProvider
Let’s enhance our SubmitOrderTest
to run with two different user accounts and product selections. We’ll create a @DataProvider
method in SubmitOrderTest
to supply the data.
- In
src/test/java/tests.SubmitOrderTest
, add a@DataProvider
method namedgetData
:
@DataProvider
public Object[][] getData() {
return new Object[][] {
{"first_email@example.com", "your_password", "ZARA COAT 3"},
{"second_email@example.com", "your_password", "ADIDAS ORIGINAL"}
};
}
Explanation:
@DataProvider
: MarksgetData
as a data provider that TestNG will use to feed data to test methods.Two-Dimensional Array:
Object[][]
is a2D
array where each inner array({...})
represents one data set. Here, we have two data sets, each with three values: email, password, and product name.Object Type: We use Object instead of String or Integer because it’s a generic data type that can hold any value (strings, integers, etc.), making the data provider flexible.
Data Sets:
First set: first_email@example.com, your_password, ZARA COAT 3
Second set: second_email@example.com, your_password, ADIDAS ORIGINAL
Attaching DataProvider to a Test
Update the submitOrder
test to use the getData
data provider:
@Test(dataProvider = "getData", groups = "Purchase")
public void submitOrder(String email, String password, String productName) {
ProductCatalogue productCatalogue = landingPage.loginApplication(email, password);
productCatalogue.addProductToCart(productName);
CartPage cartPage = productCatalogue.goToCartPage();
Boolean match = cartPage.verifyProductDisplay(productName);
Assert.assertTrue(match);
CheckoutPage checkoutPage = cartPage.goToCheckout();
checkoutPage.selectCountry("India");
ConfirmationPage confirmationPage = checkoutPage.submitOrder();
String confirmMessage = confirmationPage.getConfirmationMessage();
Assert.assertTrue(confirmMessage.equalsIgnoreCase("THANK YOU FOR THE ORDER"));
}
Explanation:
dataProvider = "getData": Links the test to the
getData
method, telling TestNG to fetch data from it.Method Parameters: The test method accepts three parameters (email, password, productName) that match the data set structure in
getData
.Dynamic Inputs: Replaces hardcoded values (e.g., "first_email@example.com", "ZARA COAT 3") with the parameters, making the test reusable.
Groups: Adds the test to a “Purchase” group for selective execution (more on this later).
Running the Test
To run only “Purchase” group tests, create a new TestNG XML file in src/test/java/testSuites
named purchase.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="PurchaseSuite">
<test name="PurchaseTests">
<groups>
<run>
<include name="Purchase"/>
</run>
</groups>
<classes>
<class name="tests.SubmitOrderTest"/>
</classes>
</test>
</suite>
Run purchase.xml
(right-click > Run As > TestNG Suite). The test should:
Run with the first data set (first_email@example.com, ZARA COAT 3), logging in, adding the product, and completing the order.
Run again with the second data set (second_email@example.com, ADIDAS ORIGINAL).
Check the TestNG results (in Eclipse’s “Results of Running Suite” tab). You’ll see two test runs:
First run: first_email@example.com, ZARA COAT 3
Second run: second_email@example.com, ADIDAS ORIGINAL
Why This Matters?
This approach is ideal for scenarios like financial applications, where you need to test the same feature (e.g., order submission) across different user accounts with varying permissions or product selections.
Supporting Multiple Groups
You can assign multiple groups to a test for flexibility. For example, to include submitOrder
in both “Purchase” and “Smoke” groups (for daily regression tests):
@Test(dataProvider = "getData", groups = {"Purchase", "Smoke"})
This allows the test to be run under different suites (e.g., a smoke suite for critical tests).
Step 2: Using HashMaps with DataProvider
The above approach works well for a few parameters, but what if a test requires 15 parameters? Listing 15 parameters in the test method signature (e.g., submitOrder(String param1, String param2, ..., String param15)) is messy and hard to maintain. A cleaner solution is to use a HashMap to store data as key-value pairs.
Modifying DataProvider to Return HashMaps
Update the getData
method in SubmitOrderTest
to return a list of HashMaps:
@DataProvider
public Object[][] getData() {
HashMap<String, String> map = new HashMap<>();
map.put("email", "first_email@example.com");
map.put("password", "your_password");
map.put("product", "ZARA COAT 3");
HashMap<String, String> map1 = new HashMap<>();
map1.put("email", "second_email@example.com");
map1.put("password", "your_password");
map1.put("product", "ADIDAS ORIGINAL");
return new Object[][] { {map}, {map1} };
}
Explanation:
HashMap Creation: Each
HashMap<String, String>
stores data as key-value pairs (e.g., "email" -> "first_email@example.com").Two HashMaps:
map
for the first data set,map1
for the second.2D Array: Wraps each HashMap in an array
({map})
, creating a 2D array with two entries.String Type: Uses String for keys and values for simplicity. (You could use
HashMap<String, Object>
for mixed data types like integers.)
Updating the Test Method
Modify submitOrder
to accept a HashMap
and extract values using keys:
@Test(dataProvider = "getData", groups = "Purchase")
public void submitOrder(HashMap<String, String> input) {
ProductCatalogue product = landingPage.loginApplication(input.get("email"), input.get("password"));
product.addProductToCart(input.get("product"));
CartPage cartPage = product.goToCartPage();
Boolean match = cartPage.verifyProductDisplay(input.get("product"));
Assert.assertTrue(match);
CheckoutPage checkoutPage = cartPage.goToCheckout();
checkoutPage.selectCountry("India");
String confirmMessage = checkoutPage.submitOrder().getConfirmationMessage();
Assert.assertTrue(confirmMessage.equalsIgnoreCase("THANK YOU FOR THE ORDER."));
}
Explanation:
HashMap Parameter: Accepts a single
HashMap<String, String>
named input instead of multiple parameters.Accessing Values: Uses
input.get("email")
,input.get("password")
, andinput.get("product")
to retrieve values by key.Cleaner Code: Makes the method more readable and scalable, as adding parameters only requires updating the
HashMap
.
Run purchase.xml
again. The test should run twice, first with map and then with map1, producing the same results but with a more maintainable structure.
Why HashMaps?
Scalability: Easily add more parameters without changing the test method signature.
Readability: Key-value pairs are intuitive and self-documenting.
Flexibility: Supports any data type if using
HashMap<String, Object>
.
Step 3: Driving Data from JSON Files
Hardcoding data in the test class (even as HashMaps) isn’t ideal, as it mixes test logic with test data. Instead, we’ll store data in an external JSON file, which is lightweight, widely used, and readable by both technical and non-technical stakeholders (e.g., business analysts). We’ll create a utility to parse the JSON and convert it into HashMaps for our DataProvider.
Creating a JSON File
In
src/main/java
, create a new package nameddata
.Inside data, create a file named
PurchaseOrder.json
:
[
{
"email": "first_email@example.com",
"password": "your_password",
"product": "ZARA COAT 3"
},
{
"email": "second_email@example.com",
"password": "your_password",
"product": "ADIDAS ORIGINAL"
}
]
Explanation:
Array Structure: The JSON starts with [], indicating an array of data sets.
Objects: Each data set is a JSON object {} with key-value pairs.
Two Data Sets: Matches the two HashMaps from Step 2, making it easy for business teams to provide data in this format.
Creating a Data Reader Utility
To parse the JSON file, we’ll create a utility class in the data
named DataReader
:
package data;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
public class DataReader {
public List<HashMap<String, String>> getJsonDataToMap(String filePath) throws IOException {
// Read JSON to String
String jsonContent = FileUtils.readFileToString(new File(filePath), StandardCharsets.UTF_8);
// Convert String to HashMap
ObjectMapper mapper = new ObjectMapper();
List<HashMap<String, String>> data = mapper.readValue(jsonContent, new TypeReference<List<HashMap<String, String>>>() {});
return data;
}
}
Explanation:
FileUtils.readFileToString
: From theorg.apache.commons.io
package, reads the JSON file into a string. Requires the commons-io dependency inpom.xml
(likely included with existing dependencies).Dynamic File Path: Uses new
File(filePath)
to accept any JSON file path.Encoding: Specifies
StandardCharsets.UTF_8
to avoid deprecation warnings and ensure proper string conversion.ObjectMapper: The
com.fasterxml.jackson.databind
package converts the JSON string into a list of HashMaps. Requires the jackson-databind dependency.TypeReference: Specifies the expected output type (
List<HashMap<String, String>>
) for Jackson to parse the JSON array into two HashMaps.Return Value: Returns a
List<HashMap<String, String>>
containing the two data sets.
Adding Jackson Dependency
If ObjectMapper isn’t recognized, add the jackson-databind dependency to pom.xml
:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version> <!-- Use the latest version -->
</dependency>
Find the latest version on Maven Repository.
Moving the Utility to BaseTest
To make the getJsonDataToMap
method accessible to all tests, move it to BaseTest
(since all test classes extend BaseTest
):
package testComponents;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.apache.commons.io.FileUtils;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import pageobjects.LandingPage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;
public class BaseTest {
public WebDriver driver;
public LandingPage landingPage;
public WebDriver initializeDriver() throws IOException {
Properties prop = new Properties();
FileInputStream fis = new FileInputStream(System.getProperty("user.dir") + "\\src\\main\\java\\resources\\GlobalData.properties");
prop.load(fis);
String browserName = prop.getProperty("browser");
if (browserName.equalsIgnoreCase("chrome")) {
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
} else if (browserName.equalsIgnoreCase("firefox")) {
WebDriverManager.firefoxdriver().setup();
driver = new FirefoxDriver();
} else if (browserName.equalsIgnoreCase("edge")) {
WebDriverManager.edgedriver().setup();
driver = new EdgeDriver();
}
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
return driver;
}
@BeforeMethod
public void launchApplication() throws IOException {
driver = initializeDriver();
landingPage = new LandingPage(driver);
landingPage.goTo();
}
@AfterMethod
public void tearDown() {
driver.quit();
}
public List<HashMap<String, String>> getJsonDataToMap(String filePath) throws IOException {
String jsonContent = FileUtils.readFileToString(new File(filePath), StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper();
List<HashMap<String, String>> data = mapper.readValue(jsonContent, new TypeReference<List<HashMap<String, String>>>() {});
return data;
}
}
Explanation:
Inheritance: Since test classes extend
BaseTest
, they can callgetJsonDataToMap
without creating aDataReader
object.Generic Utility: Accepts a
filePath
parameter, making it reusable for any JSON file.
Updating SubmitOrderTest
Remove the hardcoded HashMaps from getData
and use getJsonDataToMap
:
@DataProvider
public Object[][] getData() throws IOException {
List<HashMap<String, String>> data = getJsonDataToMap(System.getProperty("user.dir") + "\\src\\main\\java\\data\\PurchaseOrder.json");
return new Object[][] { {data.get(0)}, {data.get(1)} };
}
Explanation:
Dynamic Path: Uses
System.getProperty("user.dir")
to build the JSON file path dynamically.getJsonDataToMap: Calls the utility to parse
PurchaseOrder.json
into aList<HashMap<String, String>>
.List Access:
data.get(0)
retrieves the firstHashMap
,data.get(1)
the second.2D Array: Wraps each
HashMap
in an array for theDataProvider
.
Run purchase.xml
. The test should run twice, reading data from PurchaseOrder.json
, producing the same results as before but with data externalized.
Why JSON?
Separation of Concerns: Keeps test data out of test code, making tests cleaner.
Accessibility: Business teams can provide data in JSON without touching code.
Reusability: The
getJsonDataToMap
utility works for any JSON file, supporting all tests.Industry Standard: JSON is lightweight and widely used, unlike Excel or CSV.
Interview Tip: Be prepared to explain how you drive data from external files (e.g., JSON) using TestNG’s DataProvider
for parameterization. Highlight the steps: JSON creation, parsing with Jackson, and integration with DataProvider.
Step 4: Building a Screenshot Utility
To improve debugging, we’ll create a utility to take screenshots when tests fail. These screenshots will later be attached to HTML reports which we will cover later.
Creating the Screenshot Method
Add a getScreenshot
method to BaseTest
to capture and save screenshots:
public String getScreenshot(String testCaseName) throws IOException {
TakesScreenshot ts = (TakesScreenshot) driver;
File source = ts.getScreenshotAs(OutputType.FILE);
File file = new File(System.getProperty("user.dir") + "\\reports\\" + testCaseName + ".png");
FileUtils.copyFile(source, file);
return System.getProperty("user.dir") + "\\reports\\" + testCaseName + ".png";
}
Explanation:
TakesScreenshot: Casts the WebDriver to
TakesScreenshot
to enable screenshot capture.getScreenshotAs: Captures the screenshot as a File (other options include bytes or base64).
File Destination: Saves the screenshot in a reports folder under the project directory, naming it with
testCaseName
(e.g.,submitOrder.png
).Dynamic Path: Uses
System.getProperty("user.dir")
for portability.FileUtils.copyFile: Copies the screenshot from source (temporary file) to file (destination).
Return Value: Returns the file path as a string for later use (e.g., attaching to reports).
Throws IOException: Handles file operations errors.
Note: The reports folder must exist in your project directory. Create it manually or add logic to create it programmatically.
Check out the complete code repository below:
Key Takeaways
DataProvider: Enables data-driven testing by supplying multiple data sets to a test method.
HashMaps: Makes tests scalable and readable by using key-value pairs instead of multiple parameters.
JSON Data: Externalizes test data for better maintainability and collaboration, parsed using Jackson’s ObjectMapper.
Screenshot Utility: Captures screenshots for failed tests, saving them with dynamic file names.
Framework Design: Utilities like
getJsonDataToMap
andgetScreenshot
are one-time implementations, reusable across all tests.TestNG Groups: Organizes tests for selective execution (e.g., “Purchase” suite).
What’s Next?
In Part 5, we’ll:
We will discuss about the basics of Extent Reports.
Integrate Extent Reports into the framework to generate HTML reports.
Attach screenshots to reports for failed tests.
Explore CI/CD integration with tools like Jenkins.
Add advanced parameterization techniques.
Stay tuned for the next part of Selenium Framework Design!
Happy testing!
Subscribe to my newsletter
Read articles from Samiksha Kute directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Samiksha Kute
Samiksha Kute
Passionate Learner!