Enhancing Your Automation Framework: Using TestNG Features (Part 3)

Table of contents
- Recap: What We’ve Done So Far
- Step 1: Creating a BaseTest Class
- Step 2: Setting Up Global Properties
- Step 3: Launching the Application
- Step 4: Integrating BaseTest with TestNG
- Step 5: Using TestNG Before/After Annotations
- Step 6: Adding Error Validation Tests
- Step 7: Defining a Test Strategy
- Step 8: Running Tests with TestNG XML
- Step 9: Writing Dependent Tests
- Step 10: Running Tests in Parallel
- Step 11: Running Selective Tests with Grouping
- Final Framework Structure
- Key Takeaways
- What’s Next?

Welcome back to the third installment of our Selenium Framework Design! In Part 1, we created a standalone Selenium test for an e-commerce application, and in Part 2, we transformed it into a professional framework using the Page Object Model (POM). Now, in Part 3, we’ll enhance our framework by adding a BaseTest class for reusable test setup, implementing TestNG for better test management, creating a test strategy, and exploring advanced features like parallel execution, grouping, and dependent tests. This guide will walk you through every step to make your framework robust and scalable. Let’s dive in!
Recap: What We’ve Done So Far
In Part 2, we:
Organized our test using POM, creating page object classes (
LandingPage
,ProductCatalogue
, etc.) undersrc/main/java/pageobjects
.Built an
AbstractComponent
parent class to store reusable methods likewaitForElementToAppear
andwaitForElementToDisappear
.Refactored our standalone test (
SubmitOrderTest
) to use page objects, making it cleaner and more maintainable.
However, our test still has repetitive code, like driver initialization and browser setup, which we’ll address now. We’ll also introduce TestNG to run multiple tests efficiently and explore how to structure tests logically.
Step 1: Creating a BaseTest Class
Every test case needs to set up a WebDriver, configure timeouts, and maximize the browser window. Repeating this code in every test is inefficient, especially if you have 100 test cases. To solve this, we’ll create a BaseTest
class to centralize all reusable test setup code.
Setting Up the testComponents Package
Since test-related code belongs in src/test/java
, we’ll create a new package for test utilities:
In
src/test/java
, create a package calledtestComponents
.Create another package called tests to store all test cases live under tests.
Creating the BaseTest Class
Inside testComponents
, create a new Java class named BaseTest
. This class will handle driver initialization and other common setup tasks.
Initializing the Driver
Let’s move the driver setup code from SubmitOrderTest
to BaseTest
. Here’s the initial code we’re working with:
WebDriverManager.chromedriver().setup();
WebDriver driver = new ChromeDriver();
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
We’ll create a method called initializeDriver
in BaseTest
to handle this:
package testComponents;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import java.time.Duration;
public class BaseTest {
public WebDriver driver;
public WebDriver initializeDriver() {
driver = new ChromeDriver();
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
return driver;
}
}
Explanation:
Global WebDriver: We declare public WebDriver driver at the class level to make it accessible across methods.
initializeDriver()
: Sets up theChromeDriver
, maximizes the window, sets an implicit wait, and returns the driver for use in tests.
This works for Chrome, but what if we want to support multiple browsers (e.g., Firefox, Edge)? Let’s make it dynamic using a global properties file.
Step 2: Setting Up Global Properties
To make our framework flexible, we’ll use a properties file to specify the browser at runtime. This way, we can switch between Chrome, Firefox, or Edge without changing the code.
Creating the Properties File
In
src/main/java
, create a new package called resources.Inside resources, create a new file named
GlobalData.properties
(ensure it has the .properties extension).Add a key-value pair to specify the browser:
browser=chrome
This file controls the framework’s behavior. If you change browser=firefox
, the framework will run tests in Firefox.
Reading the Properties File
Java’s Properties class (from java.util
) can read .properties
files. We’ll modify BaseTest
to load GlobalData.properties
and initialize the driver based on the browser value.
Here’s the updated BaseTest
:
package testComponents;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import java.io.FileInputStream;
import java.io.IOException;
import java.time.Duration;
import java.util.Properties;
public class BaseTest {
public WebDriver driver;
public WebDriver initializeDriver() throws IOException {
// Load properties file
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");
// Initialize driver based on 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();
}
// Common setup
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
return driver;
}
}
Explanation:
Properties Object: Creates a Properties object to read the file.
FileInputStream: Converts the
GlobalData.properties
file into an input stream, whichprop.load()
requires.Dynamic Path: Uses
System.getProperty("user.dir")
to get the project’s root directory dynamically, avoiding hardcoded paths. Double backslashes (\) ensure Java recognizes the path.Browser Selection: Reads the browser property and initializes the appropriate driver (Chrome, Firefox, or Edge).
Common Setup: Applies
maximize()
andimplicitlyWait
regardless of the browser.
Why Dynamic Paths?
Hardcoding paths (e.g., C:\Users\YourName\Project\...
) makes the framework brittle, as paths vary across systems. System.getProperty("user.dir")
retrieves the project path dynamically, ensuring portability.
Step 3: Launching the Application
Every test for our e-commerce app starts by navigating to the landing page. Instead of repeating this in every test, we’ll add a launchApplication
method to BaseTest
that initializes the driver and opens the landing page.
package testComponents;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import pageobjects.LandingPage;
import java.io.FileInputStream;
import java.io.IOException;
import java.time.Duration;
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;
}
public LandingPage launchApplication() throws IOException {
driver = initializeDriver();
landingPage = new LandingPage(driver);
landingPage.goTo();
return landingPage;
}
}
Explanation:
Global
LandingPage
: Declarespublic LandingPage landingPage
at the class level for access across methods.launchApplication()
: Initializes the driver, creates aLandingPage
object, navigates to the URL (viagoTo()
), and returns theLandingPage
object.throws IOException
: Handles potential errors from reading the properties file.
Step 4: Integrating BaseTest with TestNG
Our SubmitOrderTest
currently uses a main method, which isn’t ideal for running multiple tests. We’ll convert it to a TestNG test and make it inherit from BaseTest
to reuse launchApplication
.
Updating SubmitOrderTest
Make
SubmitOrderTest
extendBaseTest
.Replace the main method with a TestNG
@Test
method.Remove boilerplate code (driver setup, URL navigation) and use
launchApplication
.
Here’s the updated SubmitOrderTest
:
package tests;
import org.testng.Assert;
import org.testng.annotations.Test;
import testComponents.BaseTest;
import pageobjects.*;
public class SubmitOrderTest extends BaseTest {
@Test
public void submitOrder() throws IOException {
LandingPage landingPage = launchApplication();
ProductCatalogue productCatalogue = landingPage.loginApplication("your_email@example.com", "your_password");
productCatalogue.addProductToCart("ZARA COAT 3");
CartPage cartPage = productCatalogue.goToCartPage();
Boolean match = cartPage.verifyProductDisplay("ZARA COAT 3");
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:
Extends
BaseTest
: InheritslaunchApplication
and other methods.@Test
Annotation: MarkssubmitOrder
as a TestNG test, eliminating the need for public static void main.launchApplication()
: Replaces driver setup and URL navigation, returning aLandingPage
object.Driver Close: We’ll handle this later with TestNG annotations.
Run the test to ensure it works. It should log in, add “ZARA COAT 3,” complete checkout, and verify the order, all while using the BaseTest
setup.
Step 5: Using TestNG Before/After Annotations
To further optimize, we’ll use TestNG’s @BeforeMethod
and @AfterMethod
annotations to handle setup and teardown automatically.
Adding @BeforeMethod
We’ll move the application launch logic to a @BeforMethod
in the BaseTest class, ensuring it runs automatically before every test method.
Here’s the implementation of the BaseTest:
@BeforeMethod
public void launchApplication() throws IOException {
WebDriver driver = initializeDriver();
landingPage = new LandingPage(driver);
landingPage.goTo();
}
Adding @AfterMethod
Add a method to close the browser after each test:
@AfterMethod
public void tearDown() {
driver.quit();
}
Here’s the updated BaseTest
:
package testComponents;
import java.io.FileInputStream;
import java.io.IOException;
import java.time.Duration;
import java.util.Properties;
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.BeforeTest;
import pageobjects.LandingPage;
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")) {
driver = new ChromeDriver();
} else if (browserName.equalsIgnoreCase("firefox")) {
driver = new FirefoxDriver();
} else if (browserName.equalsIgnoreCase("edge")) {
driver = new EdgeDriver();
}
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
return driver;
}
@BeforeMethod
public void launchApplication() throws IOException {
WebDriver driver = initializeDriver();
landingPage = new LandingPage(driver);
landingPage.goTo();
}
@AfterMethod
public void tearDown() {
driver.close();
}
}
Updating SubmitOrderTest
Since @BeforeMethod
handles launchApplication
, remove it from submitOrder
. Also, access the landingPage
variable directly from BaseTest
:
package tests;
import org.testng.Assert;
import org.testng.annotations.Test;
import testComponents.BaseTest;
import pageobjects.*;
public class SubmitOrderTest extends BaseTest {
@Test
public void submitOrder() {
ProductCatalogue productCatalogue = landingPage.loginApplication("your_email@example.com", "your_password");
productCatalogue.addProductToCart("ZARA COAT 3");
CartPage cartPage = productCatalogue.goToCartPage();
Boolean match = cartPage.verifyProductDisplay("ZARA COAT 3");
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:
@BeforeMethod
: Runs before each test, initializing the driver and navigating to the landing page.@AfterMethod
: Closes the browser after each test.Parent Access: Since
SubmitOrderTest
extendsBaseTest
, it can uselandingPage
directly.
Run the test again. The browser should open, execute the test, and close automatically, confirming our annotations work.
Step 6: Adding Error Validation Tests
Let’s create a new test class, ErrorValidationsTest
, to validate error scenarios, such as incorrect login credentials.
Creating ErrorValidationsTest
In
src/test/java/tests
, create a new class namedErrorValidationsTest
that extendsBaseTest
.Add a test to check the error message for an invalid login.
First, we need a locator for the error message on the login page. Inspect the error toast (“Incorrect email or password”) and note its class (trigger-flyout
). We’ll use a CSS selector to locate it.
Updating LandingPage
Add the error message locator and a method to LandingPage
:
package pageobjects;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import abstractComponents.AbstractMethod;
public class LandingPage extends AbstractMethod {
WebDriver driver;
public LandingPage(WebDriver driver) {
// initialization code
super(driver);
this.driver = driver;
PageFactory.initElements(driver, this);
}
@FindBy(id = "userEmail")
WebElement userEmail;
@FindBy(id = "userPassword")
WebElement userPassword;
@FindBy(id = "login")
WebElement submitButton;
@FindBy(css = "[class*='flyInOut']")
WebElement errorMessage;
public ProductCatalogue loginApplication(String email, String pswd) {
userEmail.sendKeys(email);
userPassword.sendKeys(pswd);
submitButton.click();
ProductCatalogue productCatalogue = new ProductCatalogue(driver);
return productCatalogue;
}
public void goTo() {
driver.get("https://rahulshettyacademy.com/client");
}
public String getErrorMessage() {
waitForWebElementToAppear(errorMessage);
return errorMessage.getText();
}
}
Explanation:
Locator: Uses
css="[class*='flyInOut']"
to match the trigger-flyout class (the asterisk * allows partial matching).getErrorMessage: Waits for the error message to appear (using
waitForWebElementToAppear
fromAbstractComponent
) and returns its text.
Updating AbstractComponent
Add a new wait method to AbstractComponent
for WebElements
:
public void waitForWebElementToAppear(WebElement webElement) {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
wait.until(ExpectedConditions.visibilityOf(webElement));
}
Writing ErrorValidationsTest
Create the test to validate the login error:
package tests;
import org.testng.Assert;
import org.testng.annotations.Test;
import testComponents.BaseTest;
public class ErrorValidationsTest extends BaseTest {
@Test
public void loginErrorValidation() {
landingPage.loginApplication("wrong_email@example.com", "WrongPassword");
String errorMessage = landingPage.getErrorMessage();
Assert.assertEquals(errorMessage, "Incorrect email or password");
}
}
Explanation:
Test Method: Attempts login with invalid credentials and verifies the error message.
No Boilerplate: Relies on
BaseTest
for setup and teardown.
Run the test. It should fail the login and validate the error message. To confirm, change the expected message (e.g., to “Wrong message”) and rerun - it should fail, proving the assertion works.
Adding Another Test
Let’s add a second test to ErrorValidationsTest
to validate a cart error (e.g., checking for a non-existent product):
@Test
public void productErrorValidation() {
String productName = "ZARA COAT 3";
ProductCatalogue productCatalogue = landingPage.loginApplication("your_email@example.com", "your_password");
productCatalogue.addProductToCart(productName);
CartPage cartPage = productCatalogue.goToCartPage();
Boolean match = cartPage.verifyProductDisplay("ZARA COAT 33");
Assert.assertFalse(match);
}
Explanation:
Scenario: Adds “ZARA COAT 3” to the cart but checks for “ZARA COAT 33,” expecting false.
Same Class: Both tests are in
ErrorValidationsTest
because they validate negative scenarios.
Step 7: Defining a Test Strategy
With multiple tests, we need a strategy to organize them logically. Creating a separate Java class for each test case (e.g., 100 classes for 100 tests) is messy. Instead, group related tests into a single class based on functionality or module.
Our Strategy
ErrorValidationsTest
: Contains tests for negative scenarios (e.g., login errors, cart errors) related to the login page or similar modules.SubmitOrderTest
: Contains tests for positive end-to-end flows (e.g., submitting an order).Module-Based Grouping: If you have tests for the product catalog, create a
ProductCatalogueTest
class with all related tests.
Why Group?
Maintainability: Grouping by module (e.g., login page) keeps code organized.
Selective Execution: If a developer changes the login page, you can run only
ErrorValidationsTest
instead of all 100 tests.Clarity: Each class represents a specific functionality (e.g., “This class handles login errors”).
Example: If you have five login error tests, put them in ErrorValidationsTest
. If you have ten product catalog tests, create a ProductCatalogueTest
class. For 100 tests, aim for ~20 classes, each handling a module or scenario type.
Tips:
Discuss with your team to decide grouping logic (e.g., by module, sprint, or scenario type).
Use unique accounts for tests to avoid conflicts (e.g., two tests adding the same product to the same cart).
Step 8: Running Tests with TestNG XML
To run all tests together, we’ll create a TestNG XML file to act as a test runner.
Creating TestNG XML
Right-click the project in Eclipse, select TestNG > Convert to TestNG.
Click Next and Finish to generate
testng.xml
.Modify
testng.xml
to include our test classes:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Suite">
<test thread-count="5" name="SubmitOrder">
<classes>
<class name="tests.SubmitOrderTest"/>
</classes>
</test>
<test thread-count="5" name="ErrorValidations">
<classes>
<class name="tests.ErrorValidationsTest"/>
</classes>
</test>
</suite>
Explanation:
Suite: The top-level container for all tests.
Test Tags: Each test tag represents a test group (e.g.,
SubmitOrder
,ErrorValidations
).Classes: Specifies the test classes to run.
Thread-Count: Limits the number of parallel threads (default is 5).
Run testng.xml
(right-click > Run As > TestNG Suite). It will execute all tests serially:
SubmitOrderTest
(1 test)ErrorValidationsTest
(2 tests)
You should see three tests pass with no failures.
Step 9: Writing Dependent Tests
Sometimes, tests depend on each other. For example, we want to verify if “ZARA COAT 3” appears in the order history after submitting an order. This requires a new test that depends on submitOrder
.
Creating OrderPage
First, create a page object for the orders page:
- In
src/main/java/pageobjects
, create a new class namedOrderPage
:
package pageobjects;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import abstractcomponents.AbstractComponent;
import java.util.List;
public class OrderPage extends AbstractComponent {
WebDriver driver;
@FindBy(css = "tr td:nth-child(3)")
List<WebElement> productNames;
public OrderPage(WebDriver driver) {
super(driver);
this.driver = driver;
PageFactory.initElements(driver, this);
}
public Boolean verifyOrderDisplay(String productName) {
return productNames.stream()
.anyMatch(product -> product.getText().equalsIgnoreCase(productName));
}
}
Explanation:
Locator: Uses
css="tr td:nth-child(3)"
to get all product names in the second column of the orders table.verifyOrderDisplay
: Checks if the specified product is in the order history.
Updating AbstractComponent
Add a locator and method for the “Orders” header link:
@FindBy(css = "[routerlink*='myorders']")
WebElement orderHeader;
public OrderPage goToOrdersPage() {
orderHeader.click();
return new OrderPage(driver);
}
Creating OrderHistoryTest
Add a new test to SubmitOrderTest
to verify the order history, with a dependency on submitOrder
:
package tests;
import org.testng.Assert;
import org.testng.annotations.Test;
import testComponents.BaseTest;
import pageobjects.*;
public class SubmitOrderTest extends BaseTest {
String productName = "ZARA COAT 3";
@Test
public void submitOrder() {
ProductCatalogue productCatalogue = landingPage.loginApplication("your_email@example.com", "your_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"));
}
@Test(dependsOnMethods = "submitOrder")
public void verifyOrderHistory() {
ProductCatalogue productCatalogue = landingPage.loginApplication("your_email@example.com", "your_password");
OrderPage orderPage = productCatalogue.goToOrdersPage();
Assert.assertTrue(orderPage.verifyOrderDisplay(productName));
}
}
Explanation:
Global Product Name: Declares
productName
at the class level for use in both tests.dependsOnMethods
: EnsuresverifyOrderHistory
runs only aftersubmitOrder
succeeds.Test Logic: Logs in, navigates to the orders page, and verifies “ZARA COAT 3” is listed.
Why Separate Tests?
Combining order submission and history verification in one test makes debugging harder. If the test fails, you won’t know which part (submission or history) is broken. Separate tests with dependencies provide clarity and focus.
Run SubmitOrderTest
. TestNG will run submitOrder
first, then verifyOrderHistory
, ensuring the order is placed before checking the history.
Step 10: Running Tests in Parallel
To speed up execution, we can run tests in parallel using testng.xml
.
Updating testng.xml
Add the parallel attribute to the suite tag:
<suite name="Suite" parallel="tests" thread-count="5">
<test name="SubmitOrder">
<classes>
<class name="tests.SubmitOrderTest"/>
</classes>
</test>
<test name="ErrorValidations">
<classes>
<class name="tests.ErrorValidationsTest"/>
</classes>
</test>
</suite>
Explanation:
parallel="tests"
: Runs each<test>
tag in parallel (e.g.,SubmitOrder
andErrorValidations
).thread-count="5"
: Limits the number of parallel threads to 5 (adjust based on system capacity).
Run testng.xml
. You should see two browsers open simultaneously, one running SubmitOrderTest
and the other ErrorValidationsTest
. This reduces execution time significantly for large test suites.
Running Methods in Parallel
To run methods within a class in parallel (e.g., loginErrorValidation
and productErrorValidation
in ErrorValidationsTest
), use parallel="methods"
:
<suite name="Suite" parallel="methods" thread-count="5">
This opens a browser for each method, up to the thread-count limit. Be cautious, as too many parallel browsers can strain your system.
Step 11: Running Selective Tests with Grouping
Sometimes, you only want to run specific tests (e.g., error validations). TestNG’s grouping feature lets you tag tests and run only those groups.
Adding Groups
Update ErrorValidationsTest
to assign tests to the “ErrorHandling” group:
@Test(groups = "ErrorHandling")
public void loginErrorValidation() {
landingPage.loginApplication("wrong_email@example.com", "WrongPassword");
String errorMessage = landingPage.getErrorMessage();
Assert.assertEquals(errorMessage, "Incorrect email or password");
}
@Test(groups = "ErrorHandling")
public void productErrorValidation() {
String productName = "ZARA COAT 3";
ProductCatalogue productCatalogue = landingPage.loginApplication("your_email@example.com", "your_password");
productCatalogue.addProductToCart(productName);
CartPage cartPage = productCatalogue.goToCartPage();
Boolean match = cartPage.verifyProductDisplay("ZARA COAT 33");
Assert.assertFalse(match);
}
Creating a Group-Specific XML
Create a new package in
src/test/java
calledtestSuites
.Inside
testSuites
, create a new file namedErrorValidations.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="ErrorValidationSuite">
<test name="ErrorValidations">
<groups>
<run>
<include name="ErrorHandling"/>
</run>
</groups>
<classes>
<class name="tests.ErrorValidationsTest"/>
</classes>
</test>
</suite>
Explanation:
groups > run > include: Specifies that only tests in the “ErrorHandling” group should run.
classes: Includes
ErrorValidationsTest
, but only methods tagged with “ErrorHandling” will execute.
Handling Before/After Methods
Running ErrorValidations.xml
may fail because @BeforeMethod
and @AfterMethod
in BaseTest
aren’t tagged with “ErrorHandling.” TestNG skips untagged methods, but we need these to run for all tests.
Update BaseTest
to use alwaysRun=true
:
@BeforeMethod(alwaysRun = true)
public void beforeMethod() throws IOException {
driver = initializeDriver();
landingPage = new LandingPage(driver);
landingPage.goTo();
}
@AfterMethod(alwaysRun = true)
public void tearDown() {
driver.quit();
}
Explanation:
alwaysRun=true
: Ensures@BeforeMethod
and@AfterMethod
run for all groups, regardless of the group filter.
Run ErrorValidations.xml
. It should execute only the two tests in ErrorValidationsTest
, with setup and teardown working correctly.
Final Framework Structure
Here’s how our project looks:
├── src/main/java
│ ├── pageobjects
│ │ ├── LandingPage.java
│ │ ├── ProductCatalogue.java
│ │ ├── CartPage.java
│ │ ├── CheckoutPage.java
│ │ ├── ConfirmationPage.java
│ │ └── OrderPage.java
│ ├── abstractComponents
│ │ └── AbstractComponent.java
│ └── resources
│ └── GlobalData.properties
├── src/test/java
│ ├── tests
│ │ ├── SubmitOrderTest.java
│ │ ├── ErrorValidationsTest.java
| | └── StandaloneTest.java
│ └── testComponents
│ └── BaseTest.java
└── testSuites
├── ErrorValidations.xml
└── testng.xml
The testng.xml
file runs all tests, while ErrorValidations.xml
runs specific groups.
Key Takeaways
BaseTest: Centralizes driver setup, browser configuration, and application launch.
Global Properties: Allows dynamic browser selection via
GlobalData.properties
.TestNG Annotations:
@BeforeMethod
and@AfterMethod
eliminate boilerplate code.Test Strategy: Group tests by module or scenario type (e.g., error validations) for maintainability.
TestNG XML: Runs all tests serially or in parallel.
Dependent Tests: Use
dependsOnMethods
to ensure tests run in the correct order.Parallel Execution: Speeds up execution with
parallel="tests"
or parallel="methods".Grouping: Runs selective tests using groups and
alwaysRun=true
for setup/teardown.
Note: If you encounter issues (e.g., test failures due to account conflicts), use unique email IDs for each test to avoid cart overlaps, especially in parallel execution.
Check out the complete code repository below:
What’s Next?
In Part 4, we’ll explore:
Data-driven testing using TestNG’s
@DataProvider
.Generating detailed TestNG reports.
Integrating with CI/CD tools like Jenkins.
Handling dynamic test data and parameterization.
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!