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

Samiksha KuteSamiksha Kute
16 min read

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.) under src/main/java/pageobjects.

  • Built an AbstractComponent parent class to store reusable methods like waitForElementToAppear and waitForElementToDisappear.

  • 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:

  1. In src/test/java, create a package called testComponents.

  2. 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 the ChromeDriver, 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

  1. In src/main/java, create a new package called resources.

  2. Inside resources, create a new file named GlobalData.properties (ensure it has the .properties extension).

  3. 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, which prop.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() and implicitlyWait 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: Declares public LandingPage landingPage at the class level for access across methods.

  • launchApplication(): Initializes the driver, creates a LandingPage object, navigates to the URL (via goTo()), and returns the LandingPage 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

  1. Make SubmitOrderTest extend BaseTest.

  2. Replace the main method with a TestNG @Test method.

  3. 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: Inherits launchApplication and other methods.

  • @Test Annotation: Marks submitOrder as a TestNG test, eliminating the need for public static void main.

  • launchApplication(): Replaces driver setup and URL navigation, returning a LandingPage 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 extends BaseTest, it can use landingPage 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

  1. In src/test/java/tests, create a new class named ErrorValidationsTest that extends BaseTest.

  2. 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 from AbstractComponent) 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

  1. Right-click the project in Eclipse, select TestNG > Convert to TestNG.

  2. Click Next and Finish to generate testng.xml.

  3. 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:

  1. In src/main/java/pageobjects, create a new class named OrderPage:
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: Ensures verifyOrderHistory runs only after submitOrder 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 and ErrorValidations).

  • 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

  1. Create a new package in src/test/java called testSuites.

  2. Inside testSuites, create a new file named ErrorValidations.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

  1. BaseTest: Centralizes driver setup, browser configuration, and application launch.

  2. Global Properties: Allows dynamic browser selection via GlobalData.properties.

  3. TestNG Annotations: @BeforeMethod and @AfterMethod eliminate boilerplate code.

  4. Test Strategy: Group tests by module or scenario type (e.g., error validations) for maintainability.

  5. TestNG XML: Runs all tests serially or in parallel.

  6. Dependent Tests: Use dependsOnMethods to ensure tests run in the correct order.

  7. Parallel Execution: Speeds up execution with parallel="tests" or parallel="methods".

  8. 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!

0
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!