Back to Chapters

Chapter 7: Test Execution Control

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