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()});
}
}
}
}