Back to Chapters

Chapter 9: Advanced TestNG Features

Groups and Dependencies

TestNG provides powerful mechanisms for organizing tests into groups and defining dependencies between tests.

Test Groups

Groups allow you to categorize tests and run specific categories as needed:

import org.testng.annotations.Test;

public class GroupsExample {

    @Test(groups = {"smoke"})
    public void smokeTest1() {
        System.out.println("Running smoke test 1");
    }

    @Test(groups = {"smoke"})
    public void smokeTest2() {
        System.out.println("Running smoke test 2");
    }

    @Test(groups = {"regression"})
    public void regressionTest1() {
        System.out.println("Running regression test 1");
    }

    @Test(groups = {"regression"})
    public void regressionTest2() {
        System.out.println("Running regression test 2");
    }

    @Test(groups = {"smoke", "regression"})
    public void bothTest() {
        System.out.println("Running test in both smoke and regression");
    }

    @Test
    public void defaultTest() {
        System.out.println("Running test without group");
    }
}

You can run specific groups using the TestNG XML file:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="GroupsSuite">
    <test name="SmokeTests">
        <groups>
            <run>
                <include name="smoke"/>
            </run>
        </groups>
        <classes>
            <class name="com.example.GroupsExample"/>
        </classes>
    </test>
</suite>

Group Dependencies

You can define dependencies between groups:

import org.testng.annotations.Test;

public class GroupDependenciesExample {

    @Test(groups = {"init"})
    public void initEnvironment() {
        System.out.println("Initializing test environment");
    }

    @Test(groups = {"init"})
    public void initDatabase() {
        System.out.println("Initializing database");
    }

    @Test(groups = {"functional"}, dependsOnGroups = {"init"})
    public void functionalTest1() {
        System.out.println("Running functional test 1");
    }

    @Test(groups = {"functional"}, dependsOnGroups = {"init"})
    public void functionalTest2() {
        System.out.println("Running functional test 2");
    }

    @Test(groups = {"performance"}, dependsOnGroups = {"functional"})
    public void performanceTest() {
        System.out.println("Running performance test");
    }
}

Method Dependencies

You can define dependencies between individual test methods:

import org.testng.annotations.Test;

public class MethodDependenciesExample {

    @Test
    public void createUser() {
        System.out.println("Creating user");
    }

    @Test(dependsOnMethods = {"createUser"})
    public void updateUser() {
        System.out.println("Updating user");
    }

    @Test(dependsOnMethods = {"updateUser"})
    public void deleteUser() {
        System.out.println("Deleting user");
    }

    @Test(dependsOnMethods = {"createUser", "updateUser", "deleteUser"})
    public void verifyUserOperations() {
        System.out.println("Verifying all user operations");
    }
}

Soft Dependencies

By default, if a test method that others depend on fails, the dependent tests are skipped. You can change this behavior using the alwaysRun attribute:

import org.testng.annotations.Test;

public class SoftDependenciesExample {

    @Test
    public void setupEnvironment() {
        System.out.println("Setting up environment");
        throw new RuntimeException("Setup failed"); // Simulate failure
    }

    @Test(dependsOnMethods = {"setupEnvironment"}, alwaysRun = true)
    public void testFeature() {
        System.out.println("Testing feature despite setup failure");
    }
}

Factory and Object Factory

TestNG provides factory methods to create test instances dynamically.

Factory Method

A factory method returns an array of test class instances:

import org.testng.annotations.Factory;
import org.testng.annotations.Test;

public class FactoryExample {

    @Factory
    public Object[] createInstances() {
        return new Object[] {
            new TestClass("Test 1"),
            new TestClass("Test 2"),
            new TestClass("Test 3")
        };
    }
}

class TestClass {
    private String name;

    public TestClass(String name) {
        this.name = name;
    }

    @Test
    public void testMethod() {
        System.out.println("Running test for: " + name);
    }
}

Object Factory

An object factory is a more flexible way to create test instances:

import org.testng.IObjectFactory;
import org.testng.annotations.ObjectFactory;
import org.testng.annotations.Test;

import java.lang.reflect.Constructor;

public class ObjectFactoryExample {

    @ObjectFactory
    public IObjectFactory createFactory() {
        return new CustomObjectFactory();
    }

    @Test
    public void testMethod() {
        System.out.println("Running test method");
    }
}

class CustomObjectFactory implements IObjectFactory {

    @Override
    public Object newInstance(Constructor constructor, Object... params) {
        try {
            System.out.println("Creating instance of: " + constructor.getDeclaringClass().getName());
            return constructor.newInstance(params);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Parameterized Factory

You can combine factories with parameters:

import org.testng.annotations.Factory;
import org.testng.annotations.Test;

public class ParameterizedFactoryExample {

    @Factory
    public Object[] createInstances() {
        return new Object[] {
            new ParameterizedTest(10, 20),
            new ParameterizedTest(5, 5),
            new ParameterizedTest(-1, 1)
        };
    }
}

class ParameterizedTest {
    private int a;
    private int b;

    public ParameterizedTest(int a, int b) {
        this.a = a;
        this.b = b;
    }

    @Test
    public void testAddition() {
        System.out.println(a + " + " + b + " = " + (a + b));
    }

    @Test
    public void testMultiplication() {
        System.out.println(a + " * " + b + " = " + (a * b));
    }
}

Dynamic Test Generation

TestNG allows you to generate tests dynamically at runtime.

Using IMethodSelector

You can implement the IMethodSelector interface to dynamically select which test methods to run:

import org.testng.IMethodSelector;
import org.testng.IMethodSelectorContext;
import org.testng.ITestNGMethod;
import org.testng.annotations.Test;

import java.util.List;

public class DynamicTestSelectionExample {

    @Test
    public void test1() {
        System.out.println("Running test 1");
    }

    @Test
    public void test2() {
        System.out.println("Running test 2");
    }

    @Test
    public void test3() {
        System.out.println("Running test 3");
    }
}

class RandomMethodSelector implements IMethodSelector {

    @Override
    public boolean includeMethod(IMethodSelectorContext context,
                               ITestNGMethod method,
                               boolean isTestMethod) {
        if (!isTestMethod) {
            return true; // Include non-test methods
        }

        // Randomly include or exclude test methods
        return Math.random() > 0.5;
    }

    @Override
    public void setTestMethods(List<ITestNGMethod> testMethods) {
        // Not used in this example
    }
}

You can configure the method selector in the TestNG XML file:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="DynamicSuite">
    <method-selectors>
        <method-selector>
            <selector-class name="com.example.RandomMethodSelector"/>
        </method-selector>
    </method-selectors>
    <test name="DynamicTest">
        <classes>
            <class name="com.example.DynamicTestSelectionExample"/>
        </classes>
    </test>
</suite>

Using IHookable

The IHookable interface allows you to intercept test method execution:

import org.testng.IHookCallBack;
import org.testng.IHookable;
import org.testng.ITestResult;
import org.testng.annotations.Test;

public class HookableExample implements IHookable {

    @Override
    public void run(IHookCallBack callBack, ITestResult testResult) {
        // Before test execution
        System.out.println("Before running: " + testResult.getMethod().getMethodName());

        // Decide whether to run the test
        if (shouldRunTest(testResult)) {
            callBack.runTestMethod(testResult);
        } else {
            System.out.println("Skipping test: " + testResult.getMethod().getMethodName());
            testResult.setStatus(ITestResult.SKIP);
        }

        // After test execution
        System.out.println("After running: " + testResult.getMethod().getMethodName());
    }

    private boolean shouldRunTest(ITestResult testResult) {
        // Dynamic decision logic
        return Math.random() > 0.3; // 70% chance of running the test
    }

    @Test
    public void test1() {
        System.out.println("Running test 1");
    }

    @Test
    public void test2() {
        System.out.println("Running test 2");
    }

    @Test
    public void test3() {
        System.out.println("Running test 3");
    }
}

Custom Annotations

You can create custom annotations to extend TestNG's functionality.

Creating a Custom Annotation

import org.testng.IAnnotationTransformer;
import org.testng.annotations.ITestAnnotation;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@interface Retry {
    int times() default 3;
}

@Listeners(RetryAnnotationTransformer.class)
public class CustomAnnotationExample {

    @Test
    @Retry(times = 5)
    public void flakyTest() {
        System.out.println("Running flaky test");
        double random = Math.random();
        if (random < 0.8) {
            throw new RuntimeException("Random failure");
        }
    }

    @Test
    public void normalTest() {
        System.out.println("Running normal test");
    }
}

class RetryAnnotationTransformer implements IAnnotationTransformer {

    @Override
    public void transform(ITestAnnotation annotation,
                         Class testClass,
                         Constructor testConstructor,
                         Method testMethod) {
        if (testMethod != null) {
            Retry retry = testMethod.getAnnotation(Retry.class);
            if (retry != null) {
                annotation.setRetryAnalyzer(CustomRetryAnalyzer.class);
                // Store retry count in attribute for later use
                annotation.setAttribute("retryTimes", retry.times());
            }
        }
    }
}

class CustomRetryAnalyzer implements org.testng.IRetryAnalyzer {

    private int retryCount = 0;

    @Override
    public boolean retry(org.testng.ITestResult result) {
        // Get retry times from test annotation
        int maxRetry = 3; // Default
        Object retryTimes = result.getMethod().getConstructorOrMethod()
                .getMethod().getAnnotation(ITestAnnotation.class)
                .getAttribute("retryTimes");

        if (retryTimes != null) {
            maxRetry = (Integer) retryTimes;
        }

        if (retryCount < maxRetry) {
            retryCount++;
            System.out.println("Retrying test: " + result.getName() +
                               ", retry count: " + retryCount);
            return true; // Retry the test
        }
        return false; // No more retries
    }
}

Using Java 21 Features with Custom Annotations

Java 21's enhanced type inference and pattern matching can improve custom annotation handling:

import org.testng.IAnnotationTransformer;
import org.testng.annotations.ITestAnnotation;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@interface Performance {
    enum Level { LOW, MEDIUM, HIGH }
    Level expectation() default Level.MEDIUM;
    long timeoutMs() default 1000;
}

@Listeners(PerformanceAnnotationTransformer.class)
public class Java21AnnotationExample {

    @Test
    @Performance(expectation = Performance.Level.HIGH, timeoutMs = 500)
    public void highPerformanceTest() {
        System.out.println("Running high performance test");
        // Test logic
    }

    @Test
    @Performance(expectation = Performance.Level.LOW, timeoutMs = 5000)
    public void lowPerformanceTest() {
        System.out.println("Running low performance test");
        // Test logic
    }

    @Test
    public void normalTest() {
        System.out.println("Running normal test");
        // Test logic
    }
}

class PerformanceAnnotationTransformer implements IAnnotationTransformer {

    // Using Java 21 record for performance configuration
    record PerformanceConfig(Performance.Level level, long timeoutMs) {}

    // Store configurations for later use
    private final Map<String, PerformanceConfig> configs = new HashMap<>();

    @Override
    public void transform(ITestAnnotation annotation,
                         Class testClass,
                         Constructor testConstructor,
                         Method testMethod) {
        if (testMethod != null) {
            Performance performance = testMethod.getAnnotation(Performance.class);

            if (performance != null) {
                // Set timeout from annotation
                annotation.setTimeOut(performance.timeoutMs());

                // Store configuration for later use
                String methodKey = testMethod.getDeclaringClass().getName() + "." + testMethod.getName();
                configs.put(methodKey, new PerformanceConfig(
                    performance.expectation(),
                    performance.timeoutMs()
                ));

                // Set listener for performance validation
                annotation.setListeners(new String[]{PerformanceListener.class.getName()});
            }
        }
    }
}