Monday, July 1, 2024

Unit Testing of ExecutorService in Java With No Thread sleep

Unit Testing of ExecutorService in Java With No Thread sleep

Unit testing concurrent code, especially code utilizing ExecutorService, presents unique challenges due to its asynchronous nature. Traditional approaches often involve using Thread.sleep() to wait for tasks to be completed, but this method is unreliable and can lead to flaky tests. In this article, we’ll explore alternative strategies to unit test ExecutorService without relying on Thread sleep method. This ensures reliable tests that do not depend on arbitrary sleep durations.

1. Understanding ExecutorService


ExecutorService is a framework in Java for executing tasks asynchronously. It manages a pool of threads and allows you to submit tasks for concurrent execution. Testing code that uses ExecutorService typically involves verifying that tasks are executed correctly and that the service behaves as expected under various conditions.

1.1 Challenges with Thread.sleep()

Using Thread.sleep() in tests introduces several issues:

  • Non-deterministic Tests: Timing-based tests can be unpredictable and may fail randomly due to variations in thread scheduling and execution speed.
  • Slow Tests: Sleeping for a fixed duration can make tests unnecessarily slow, especially if tasks complete quickly or if longer delays are required to ensure completion.

2. Alternative Approaches to Unit Testing ExecutorService


To write reliable tests for ExecutorService without Thread.sleep(), consider the following approaches. First, we create a MyRunnable class that implements the Runnable interface and performs a long-running calculation (In this article, we are calculating the sum of a large range of numbers).

MyRunnable.java

public class MyRunnable implements Runnable {
 
    private final long start;
    private final long end;
    private long result;
 
    public MyRunnable(long start, long end) {
        this.start = start;
        this.end = end;
    }
 
    @Override
    public void run() {
        result = 0;
        for (long i = start; i <= end; i++) {
            result += i;
        }
        System.out.println("Calculation complete. Result: " + result);
    }
 
    public long getResult() {
        return result;
    }
}

2.1 Use Future to Get the Result

To get the result of the task and ensure completion, we can use Future.

FutureExampleTest.java

public class FutureExampleTest {
     
    @Test
    public void testFutureWithLongRunningCalculation() throws Exception {
         
        ExecutorService executor = Executors.newSingleThreadExecutor();
 
        // Create an instance of MyRunnable with a long-running calculation
        MyRunnable task = new MyRunnable(1, 1000000000L);
 
        // Submit the task to the executor and get a Future
        Future<?> future = executor.submit(task);
 
        // Wait for the task to complete and get the result
        future.get(); // Blocks until the task completes
 
        // Verify the result
        long expected = (1000000000L * (1000000000L + 1)) / 2;
        assertEquals(expected, task.getResult());
 
        // Shutdown the executor
        executor.shutdown();
    }
     
}

In this example, we submit the MyRunnable task to the executor and get a Future object. The future.get() method blocks until the task is completed, ensuring we can retrieve the result after completion.

2.2 Use CountDownLatch for Synchronization

To ensure the parent thread waits for the task to complete without using Thread.sleep(), we can use CountDownLatch.

ExecutorServiceExampleTest.java

public class ExecutorServiceExampleTest {
     
    @Test
    public void testExecutorServiceWithLongRunningCalculation() throws InterruptedException {
         
        ExecutorService executor = Executors.newSingleThreadExecutor();
        CountDownLatch latch = new CountDownLatch(1);
 
        // Create a runnable with a long-running calculation
        MyRunnable task = new MyRunnable(1, 1000000000L) {
            @Override
            public void run() {
                super.run();
                latch.countDown();
            }
        };
 
        // Submit the task to the executor
        executor.submit(task);
 
        // Wait for the task to complete
        assertTrue(latch.await(2, TimeUnit.MINUTES));
 
        // Verify the result
        long expected = (1000000000L * (1000000000L + 1)) / 2;
        assertEquals(expected, task.getResult());
 
        // Shutdown the executor
        executor.shutdown();
    }
     
}

This approach uses a CountDownLatch to synchronize the completion of the task. First, we create a CountDownLatch with a count of 1 and define an anonymous subclass of MyRunnable that counts down the latch when the task completes.

Next, we submit this task to the executor and use latch.await() to wait for the task to complete, verifying with assertTrue that the task finishes within the specified timeout. After the task is completed, we verify the result using assertEquals. Finally, we shut down the executor.

2.3 Use Shutdown and Await Termination

To ensure the executor shuts down gracefully after the tasks complete, use shutdown and awaitTermination.

ShutDownExampleTest.java

public class ShutDownExampleTest {
     
    @Test
    public void testShutdownWithLongRunningCalculation() throws InterruptedException {
         
        ExecutorService executor = Executors.newSingleThreadExecutor();
 
        // Create an instance of MyRunnable with a long-running calculation
        MyRunnable task = new MyRunnable(1, 1000000000L);
 
        // Submit the task to the executor
        executor.submit(task);
 
        // Shutdown the executor
        executor.shutdown();
 
        // Wait for existing tasks to complete
        assertTrue(executor.awaitTermination(2, TimeUnit.MINUTES));
 
        // Verify the result
        long expected = (1000000000L * (1000000000L + 1)) / 2;
        assertEquals(expected, task.getResult());
    }    
}

In this approach, we ensure the executor shuts down gracefully by calling shutdown() and then awaitTermination() to wait for existing tasks to complete. If tasks do not complete within the specified timeout, we call shutdownNow() to cancel currently executing tasks and wait again.

3. Conclusion

Unit testing concurrent code with ExecutorService requires careful synchronization to ensure tests are reliable and deterministic. Avoiding Thread.sleep() is essential to prevent flaky tests and improve test execution speed. In this article, we used synchronization aids like CountDownLatch, Future, and shutdown with awaitTermination() to handle concurrency effectively in our tests. These approaches provide more reliable alternatives to Thread.sleep() for unit testing ExecutorService-based code in Java.

Source: javacodegeeks.com

Related Posts

0 comments:

Post a Comment