How to Test Java ExecutorService Without Thread.sleep(): Reliable Strategies

This article explains why using Thread.sleep() in Java ExecutorService unit tests is unreliable and inefficient, and introduces three robust alternatives—Future.get(), CountDownLatch, and shutdown/awaitTermination—to ensure stable, performant testing of asynchronous WebSocket query scenarios.

FunTester
FunTester
FunTester
How to Test Java ExecutorService Without Thread.sleep(): Reliable Strategies

ExecutorService Testing Challenges

ExecutorService is Java's thread‑pool framework used to manage asynchronous tasks efficiently. In the WebSocket product‑query system of the "Little Eight Supermarket", it can parallelize request sending or message processing. Unit tests aim to verify correct execution, expected results, and reliable concurrent logic.

Testing challenges

Non‑determinism : Fixed Thread.sleep() may be insufficient or wasteful, especially when network latency varies.

Performance loss : Static sleep times cannot adapt to dynamic task durations.

Concurrency complexity : Need to ensure task order and result correctness while avoiding data races.

The article presents three reliable alternatives: Future.get(), CountDownLatch, and shutdown()/awaitTermination().

Future

Future.get()

blocks the main thread until the task finishes, returning the result or throwing an exception, suitable for scenarios that need to verify return values.

The following code simulates a product‑query task and validates the WebSocket client response:

package org.funtester.performance.books.chapter05.section7;

import org.funtester.performance.books.chapter05.section2.JavaWebSocketClient;
import org.junit.jupiter.api.Test;
import com.alibaba.fastjson.JSONObject;

import java.net.URI;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
 * FunTester 使用 Future.get() 测试 WebSocket 客户端
 */
public class WebSocketFutureTest {
    @Test
    public void testWebSocketQuery() throws Exception {
        // 创建单线程线程池
        ExecutorService executor = Executors.newSingleThreadExecutor();
        // 构造 WebSocket 服务器的 URI
        URI uri = new URI("ws://localhost:12345/websocket/FunTester");
        // 创建 WebSocket 客户端实例
        JavaWebSocketClient client = new JavaWebSocketClient(uri);
        // 连接到 WebSocket 服务器
        client.connect();

        // 提交异步任务,发送请求并等待响应
        Future<JSONObject> future = executor.submit(() -> {
            JSONObject params = new JSONObject();
            params.put("name", "西瓜");
            params.put("index", 1);
            // 发送参数到服务器
            client.send(params);

            // 模拟异步等待响应,轮询检查消息
            for (int i = 0; i < 10; i++) {
                if (client.getLastMessage() != null) {
                    // 收到消息后解析为 JSONObject 并返回
                    return JSONObject.parseObject(client.getLastMessage());
                }
                Thread.sleep(100); // 每 100ms 轮询一次
            }
            return null;
        });

        // 阻塞直到异步任务完成,获取结果
        JSONObject result = future.get();
        // 断言返回结果的字段值
        assertEquals("西瓜", result.getString("Name"));
        assertEquals(45, result.getInteger("Price"));
        // 关闭 WebSocket 客户端
        client.close();
        // 关闭线程池
        executor.shutdown();
    }
}

Code analysis:

Applicable scenario : Verifying task return values, e.g., product‑query responses.

Advantage : Future.get() ensures task completion, avoiding manual sleep.

Note : Must handle InterruptedException and ExecutionException.

Limitation : If the task runs long, get() blocks the test thread, affecting efficiency.

CountDownLatch

CountDownLatch

is a synchronization aid that lets the main thread wait for a specified number of tasks to finish, suitable for multi‑task collaboration.

The code below tests multiple concurrent WebSocket queries:

package org.funtester.performance.books.chapter05.section7;

import org.funtester.performance.books.chapter05.section2.JavaWebSocketClient;
import org.junit.jupiter.api.Test;
import com.alibaba.fastjson.JSONObject;

import java.net.URI;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class WebSocketCountDownLatchTest {
    @Test
    public void testMultipleWebSocketQueries() throws Exception {
        // 创建一个固定大小为2的线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);
        // 创建一个倒计时锁存器,计数为2,用于等待2个任务完成
        CountDownLatch latch = new CountDownLatch(2);
        // WebSocket服务器的URI
        URI uri = new URI("ws://localhost:12345/websocket/FunTester");
        // 创建第一个WebSocket客户端,并重写onMessage方法
        JavaWebSocketClient client1 = new JavaWebSocketClient(uri) {
            @Override
            public void onMessage(String s) {
                super.onMessage(s);
                // 收到响应后计数减1
                latch.countDown();
            }
        };
        // 创建第二个WebSocket客户端,并重写onMessage方法
        JavaWebSocketClient client2 = new JavaWebSocketClient(uri) {
            @Override
            public void onMessage(String s) {
                super.onMessage(s);
                // 收到响应后计数减1
                latch.countDown();
            }
        };
        // 连接WebSocket服务器
        client1.connect();
        client2.connect();

        // 在线程池中提交第一个任务,发送消息
        executor.submit(() -> {
            JSONObject params = new JSONObject();
            params.put("name", "西瓜");
            params.put("index", 1);
            client1.send(params);
        });
        // 在线程池中提交第二个任务,发送消息
        executor.submit(() -> {
            JSONObject params = new JSONObject();
            params.put("name", "苹果");
            params.put("index", 2);
            client2.send(params);
        });

        // 等待2分钟,直到latch计数为0,即两个任务都完成
        assertTrue(latch.await(2, TimeUnit.MINUTES));
        // 关闭WebSocket客户端
        client1.close();
        client2.close();
        // 关闭线程池
        executor.shutdown();
    }
}

Code analysis:

Applicable scenario : Multi‑task collaboration testing, such as concurrent product queries.

Advantage : Precise control of task completion with timeout via await.

Note : Must call countDown() in each task.

Limitation : Manual counter management can be error‑prone in complex logic.

shutdown + awaitTermination

Combining ExecutorService.shutdown() and awaitTermination() is suitable for verifying that all tasks have completed without needing individual results.

The following code tests batch WebSocket queries:

package org.funtester.performance.books.chapter05.section7;

import org.funtester.performance.books.chapter05.section2.JavaWebSocketClient;
import org.junit.jupiter.api.Test;
import com.alibaba.fastjson.JSONObject;

import java.net.URI;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class WebSocketShutdownTest {
    @Test
    public void testBatchWebSocketQueries() throws Exception {
        // 创建一个固定大小为2的线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);
        // WebSocket 服务器地址
        URI uri = new URI("ws://localhost:12345/websocket/FunTester");
        // 创建 WebSocket 客户端
        JavaWebSocketClient client = new JavaWebSocketClient(uri);
        client.connect();

        // 向 WebSocket 发送两组不同参数的请求
        for (int i = 1; i <= 2; i++) {
            int index = i;
            executor.submit(() -> {
                JSONObject params = new JSONObject();
                params.put("name", index == 1 ? "西瓜" : "苹果");
                params.put("index", index);
                client.send(params);
            });
        }

        // 关闭线程池,等待所有任务完成
        executor.shutdown();
        assertTrue(executor.awaitTermination(2, TimeUnit.MINUTES)); // 等待所有任务完成
        client.close();
    }
}

Code analysis:

Applicable scenario : Verifying that all tasks finish, e.g., batch queries.

Advantage : Simple to use, no extra synchronization tools required.

Note : awaitTermination must be called after shutdown with a reasonable timeout.

Limitation : Cannot directly obtain task results, only state verification.

Custom Runnable

A generic FunTesterRunnable class simulates a time‑consuming task for testing; it can be replaced with actual query tasks and combined with MockServer to simulate various server responses.

package org.funtester.performance.books.chapter05.section7;

public class FunTesterRunnable implements Runnable {
    private final long start, end;
    private long result;

    public FunTesterRunnable(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("FunTester: 计算完成,结果: " + result);
    }

    public long getResult() {
        return result;
    }
}

Conclusion

When unit‑testing code based on ExecutorService, careful synchronization design is required for reliable and deterministic tests. Relying on Thread.sleep() leads to instability and inefficiency; using CountDownLatch, Future, and shutdown/awaitTermination provides more robust testing of concurrent logic.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavatestingconcurrencyExecutorServiceFutureCountDownLatch
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.