Parallel Execution
One of TestNG's most powerful features is its ability to run tests in parallel, which can significantly reduce test execution time. This is especially valuable when working with Java 21's virtual threads for improved concurrency.
Configuring Parallel Execution
You can configure parallel execution in the TestNG XML file using the parallel attribute of the <suite> tag:
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="ParallelSuite" parallel="methods" thread-count="5">
    <test name="ParallelTest">
        <classes>
            <class name="com.example.ParallelTestClass"/>
        </classes>
    </test>
</suite>The parallel attribute can have the following values:
- methods: Run test methods in parallel
- classes: Run test classes in parallel
- tests: Run- <test>tags in parallel
- instances: Run the same test method in different instances in parallel
Thread Count Configuration
The thread-count attribute specifies the number of threads to use for parallel execution:
<suite name="ParallelSuite" parallel="methods" thread-count="10">
    <!-- Test configuration -->
</suite>Parallel Data Provider
You can also run data provider invocations in parallel by setting the parallel attribute to true:
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
public class ParallelDataProviderExample {
    @DataProvider(name = "testData", parallel = true)
    public Object[][] createData() {
        return new Object[][] {
            {"Test 1", 1},
            {"Test 2", 2},
            {"Test 3", 3},
            {"Test 4", 4}
        };
    }
    @Test(dataProvider = "testData")
    public void testMethod(String name, int value) {
        System.out.println("Running " + name + " with value " + value +
                           " on thread " + Thread.currentThread().getId());
        // Test logic
        try {
            Thread.sleep(1000); // Simulate work
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}Thread Management
TestNG provides several ways to manage threads during test execution.
Thread Pool Size
You can control the number of threads used for a specific test method using the threadPoolSize attribute:
import org.testng.annotations.Test;
public class ThreadPoolExample {
    @Test(threadPoolSize = 3, invocationCount = 10, timeOut = 10000)
    public void testWithMultipleThreads() {
        System.out.println("Test running on thread: " + Thread.currentThread().getId());
        // Test logic
        try {
            Thread.sleep(1000); // Simulate work
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}In this example, the test method will be invoked 10 times using 3 threads.
Thread-Local Variables
When running tests in parallel, you might need to maintain thread-specific data. Java's ThreadLocal class is useful for this purpose:
import org.testng.annotations.Test;
public class ThreadLocalExample {
    private static final ThreadLocal<String> threadData = new ThreadLocal<>();
    @Test(threadPoolSize = 3, invocationCount = 6)
    public void testWithThreadLocalData() {
        long threadId = Thread.currentThread().getId();
        threadData.set("Data for thread " + threadId);
        System.out.println("Thread " + threadId + " has data: " + threadData.get());
        // Test logic
        try {
            Thread.sleep(1000); // Simulate work
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        // Verify thread data is still correct
        assert threadData.get().equals("Data for thread " + threadId);
        // Clean up
        threadData.remove();
    }
}Virtual Threads with Java 21
Java 21 introduces virtual threads, which are lightweight threads that can significantly improve the performance of concurrent applications. Let's explore how to leverage virtual threads with TestNG.
Custom Executor for Virtual Threads
You can create a custom executor service using virtual threads and integrate it with TestNG:
import org.testng.IExecutor;
import org.testng.ITestNGMethod;
import org.testng.ITestResult;
import org.testng.internal.IConfiguration;
import org.testng.internal.ITestResultNotifier;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VirtualThreadExecutor implements IExecutor {
    private ExecutorService executorService;
    public VirtualThreadExecutor() {
        // Create an executor service using virtual threads
        this.executorService = Executors.newVirtualThreadPerTaskExecutor();
    }
    @Override
    public List<ITestResult> execute(List<ITestNGMethod> methods, ITestResultNotifier notifier,
                                    IConfiguration configuration) {
        // Implementation of test execution using virtual threads
        // This is a simplified example
        for (ITestNGMethod method : methods) {
            executorService.submit(() -> {
                // Execute the test method
                // Report results to notifier
            });
        }
        // Return test results
        return null; // Actual implementation would return real results
    }
    @Override
    public void shutdown() {
        executorService.shutdown();
    }
}Using Virtual Threads for Test Tasks
You can use virtual threads directly in your test methods to perform concurrent operations:
import org.testng.Assert;
import org.testng.annotations.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class VirtualThreadTest {
    @Test
    public void testWithVirtualThreads() throws Exception {
        List<String> results = new ArrayList<>();
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<Future<String>> futures = new ArrayList<>();
            // Submit multiple tasks to run concurrently
            for (int i = 0; i < 100; i++) {
                final int taskId = i;
                futures.add(executor.submit(() -> {
                    // Simulate some work
                    Thread.sleep(100);
                    return "Task " + taskId + " completed";
                }));
            }
            // Collect all results
            for (Future<String> future : futures) {
                results.add(future.get());
            }
        }
        Assert.assertEquals(results.size(), 100, "All tasks should complete");
    }
}Structured Concurrency with Java 21
Java 21 introduces structured concurrency, which provides a more manageable approach to concurrent programming:
import org.testng.Assert;
import org.testng.annotations.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class StructuredConcurrencyTest {
    @Test
    public void testWithStructuredConcurrency() throws Exception {
        List<String> results = new ArrayList<>();
        // Using try-with-resources for automatic resource cleanup
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<Future<String>> futures = new ArrayList<>();
            // First group of tasks
            for (int i = 0; i < 50; i++) {
                final int taskId = i;
                futures.add(executor.submit(() -> {
                    Thread.sleep(100);
                    return "Group 1 - Task " + taskId;
                }));
            }
            // Second group of tasks
            for (int i = 50; i < 100; i++) {
                final int taskId = i;
                futures.add(executor.submit(() -> {
                    Thread.sleep(150);
                    return "Group 2 - Task " + taskId;
                }));
            }
            // Collect all results
            for (Future<String> future : futures) {
                results.add(future.get());
            }
        } // executor is automatically closed here
        Assert.assertEquals(results.size(), 100, "All tasks should complete");
        // Verify results from both groups
        long group1Count = results.stream()
                .filter(s -> s.startsWith("Group 1"))
                .count();
        long group2Count = results.stream()
                .filter(s -> s.startsWith("Group 2"))
                .count();
        Assert.assertEquals(group1Count, 50, "Should have 50 tasks from Group 1");
        Assert.assertEquals(group2Count, 50, "Should have 50 tasks from Group 2");
    }
}Timeouts and Performance
TestNG allows you to set timeouts for test methods, which can help identify performance issues.
Setting Timeouts
You can set a timeout for a test method using the timeOut attribute:
import org.testng.annotations.Test;
public class TimeoutExample {
    @Test(timeOut = 1000) // Timeout in milliseconds
    public void fastTest() {
        // This test should complete within 1 second
        System.out.println("Fast test running");
    }
    @Test(timeOut = 5000)
    public void slowTest() throws InterruptedException {
        // This test has up to 5 seconds to complete
        System.out.println("Slow test running");
        Thread.sleep(3000); // Simulate work
    }
}Measuring Test Performance
You can use TestNG listeners to measure and report test performance:
import org.testng.IInvokedMethod;
import org.testng.IInvokedMethodListener;
import org.testng.ITestResult;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(PerformanceListener.class)
public class PerformanceExample {
    @Test
    public void testMethod1() throws InterruptedException {
        // Test logic
        Thread.sleep(100); // Simulate work
    }
    @Test
    public void testMethod2() throws InterruptedException {
        // Test logic
        Thread.sleep(200); // Simulate work
    }
}
class PerformanceListener implements IInvokedMethodListener {
    @Override
    public void beforeInvocation(IInvokedMethod method, ITestResult testResult) {
        testResult.setAttribute("startTime", System.currentTimeMillis());
    }
    @Override
    public void afterInvocation(IInvokedMethod method, ITestResult testResult) {
        long startTime = (Long) testResult.getAttribute("startTime");
        long endTime = System.currentTimeMillis();
        long duration = endTime - startTime;
        System.out.println("Method " + method.getTestMethod().getMethodName() +
                           " took " + duration + " ms");
    }
}Test Retry Logic
Sometimes tests fail due to environmental issues rather than actual bugs. TestNG provides a way to automatically retry failed tests.
Implementing IRetryAnalyzer
You can implement the IRetryAnalyzer interface to define retry logic:
import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;
import org.testng.annotations.Test;
public class RetryExample {
    @Test(retryAnalyzer = TestRetryAnalyzer.class)
    public void flakyTest() {
        // Simulate a flaky test that sometimes fails
        double random = Math.random();
        if (random < 0.7) {
            throw new RuntimeException("Random failure");
        }
        System.out.println("Test passed");
    }
}
class TestRetryAnalyzer implements IRetryAnalyzer {
    private int retryCount = 0;
    private static final int MAX_RETRY_COUNT = 3;
    @Override
    public boolean retry(ITestResult result) {
        if (retryCount < MAX_RETRY_COUNT) {
            retryCount++;
            System.out.println("Retrying test: " + result.getName() +
                               ", retry count: " + retryCount);
            return true; // Retry the test
        }
        return false; // No more retries
    }
}Applying Retry Logic to All Tests
You can apply retry logic to all tests using an annotation transformer:
import org.testng.IAnnotationTransformer;
import org.testng.annotations.ITestAnnotation;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
@Listeners(RetryTransformer.class)
public class GlobalRetryExample {
    @Test
    public void test1() {
        // Test logic
        double random = Math.random();
        if (random < 0.3) {
            throw new RuntimeException("Random failure in test1");
        }
    }
    @Test
    public void test2() {
        // Test logic
        double random = Math.random();
        if (random < 0.3) {
            throw new RuntimeException("Random failure in test2");
        }
    }
}
class RetryTransformer implements IAnnotationTransformer {
    @Override
    public void transform(ITestAnnotation annotation,
                         Class testClass,
                         Constructor testConstructor,
                         Method testMethod) {
        annotation.setRetryAnalyzer(TestRetryAnalyzer.class);
    }
}Test Execution Order
TestNG allows you to control the order in which tests are executed.
Using Priorities
You can set the execution priority of test methods using the priority attribute:
import org.testng.annotations.Test;
public class PriorityExample {
    @Test(priority = 1)
    public void testA() {
        System.out.println("Test A running");
    }
    @Test(priority = 2)
    public void testB() {
        System.out.println("Test B running");
    }
    @Test(priority = 0) // Lowest priority, runs first
    public void testC() {
        System.out.println("Test C running");
    }
}Preserving Order
By default, TestNG does not guarantee the execution order of test methods with the same priority. You can preserve the order by setting the preserve-order attribute in the TestNG XML file:
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="OrderedSuite">
    <test name="OrderedTest" preserve-order="true">
        <classes>
            <class name="com.example.OrderedTestClass"/>
        </classes>
    </test>
</suite>Using Method Interceptors
You can implement the IMethodInterceptor interface to have fine-grained control over the test execution order:
import org.testng.IMethodInstance;
import org.testng.IMethodInterceptor;
import org.testng.ITestContext;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
import java.util.Comparator;
import java.util.List;
@Listeners(AlphabeticalOrderInterceptor.class)
public class MethodInterceptorExample {
    @Test
    public void testC() {
        System.out.println("Test C running");
    }
    @Test
    public void testA() {
        System.out.println("Test A running");
    }
    @Test
    public void testB() {
        System.out.println("Test B running");
    }
}
class AlphabeticalOrderInterceptor implements IMethodInterceptor {
    @Override
    public List<IMethodInstance> intercept(List<IMethodInstance> methods, ITestContext context) {
        // Sort methods alphabetically by method name
        methods.sort(Comparator.comparing(m -> m.getMethod().getMethodName()));
        return methods;
    }
}