📝 面试求职: 「面试试题小程序」 ,内容涵盖 测试基础、Linux操作系统、MySQL数据库、Web功能测试、接口测试、APPium移动端测试、Python知识、Selenium自动化测试相关、性能测试、性能测试、计算机网络知识、Jmeter、HR面试,命中率杠杠的。(大家刷起来…)
📝 职场经验干货:
在 Java 中使用 ExecutorService
管理异步任务时,单元测试需要验证任务的执行状态、顺序或结果。然而,依赖 Thread.sleep()
等待任务完成不仅不可靠,还会导致测试效率低下或间歇性失败。本文结合小八超市的 WebSocket 商品查询场景,介绍几种替代 Thread.sleep()
的优雅测试策略,确保测试的稳定性与效率。这些策略特别适用于分布式系统或高并发场景下的性能测试。
ExecutorService 测试挑战
ExecutorService
是 Java 提供的线程池框架,用于高效管理异步任务。在小八超市的 WebSocket 商品查询系统中,ExecutorService
可用于并行发送查询请求或处理推送消息。单元测试的目标是验证任务是否正确执行、结果是否符合预期,以及并发逻辑是否可靠。
测试挑战:
-
非确定性:
Thread.sleep()
的固定等待时间可能不足以覆盖任务执行,或过长导致测试时间浪费。例如,测试 WebSocket 请求可能因网络延迟而失败。 -
性能低下:固定休眠时间无法适应任务执行时间的动态变化,影响测试效率。
-
并发复杂性:多线程环境下,需确保任务完成顺序和结果正确性,避免数据竞争。
以下介绍三种更可靠的替代方案:Future.get()
、 CountDownLatch
和 shutdown()/awaitTermination()
。
Future
Future.get()
阻塞主线程直到任务完成,返回任务结果或抛出异常,适合需要验证返回值的场景。
以下代码模拟小八超市的商品查询任务,验证 WebSocket 客户端是否正确返回商品价格:
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;
importstatic org.junit.jupiter.api.Assertions.assertEquals;
/**
* FunTester 使用 Future.get() 测试 WebSocket 客户端
*/
publicclass WebSocketFutureTest {
// 使用 JUnit 的 @Test 注解标记测试方法
@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 轮询一次
}
returnnull;
});
// 阻塞直到异步任务完成,获取结果
JSONObject result = future.get();
// 断言返回结果的字段值
assertEquals("西瓜", result.getString("Name"));
assertEquals(45, result.getInteger("Price"));
// 关闭 WebSocket 客户端
client.close();
// 关闭线程池
executor.shutdown();
}
}
代码分析:
-
适用场景:适合验证任务返回值的场景,如小八超市的商品查询响应。
-
优势:
Future.get()
确保任务完成,避免手动设置休眠时间。 -
注意点:需处理可能的
InterruptedException
和ExecutionException
。在小八超市场景中,可结合 MockServer 模拟响应,验证客户端对不同价格的处理。 -
局限性:若任务长时间未完成,
get()
会阻塞测试线程,可能影响效率。
CountDownLatch
CountDownLatch
是一种同步工具,允许主线程等待指定数量的任务完成,适合多任务协作场景。
以下代码测试小八超市的多个 WebSocket 客户端并发查询:
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;
importstatic org.junit.jupiter.api.Assertions.assertTrue;
/**
* FunTester 使用 CountDownLatch 测试 WebSocket 客户端
*/
@Test
public void testMultipleWebSocketQueries() throws Exception {
// 创建一个固定大小为2的线程池
ExecutorService executor = Executors.newFixedThreadPool(2);
// 创建一个倒计时锁存器,计数为2,用于等待2个任务完成
CountDownLatch latch = new CountDownLatch(2); // 等待 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();
}
Case 代码解读:
-
适用场景:适合多任务协作测试,如小八超市的多个用户并发查询商品价格。
-
优势:
CountDownLatch
精确控制任务完成时机,支持超时机制(await
指定时间),避免无限等待。 -
注意点:需在任务中调用
countDown()
,确保逻辑正确。在小八超市场景中,可验证多个客户端是否都能收到正确响应。 -
局限性:需手动维护计数器,逻辑较复杂时易出错。
shutdown + awaitTermination
ExecutorService.shutdown()
和 awaitTermination()
组合适合验证所有任务是否完成,无需获取具体返回值。
以下代码测试小八超市的 WebSocket 客户端批量查询:
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;
importstatic org.junit.jupiter.api.Assertions.assertTrue;
/**
* FunTester 使用 shutdown() + awaitTermination() 测试 WebSocket 客户端
*/
publicclass WebSocketShutdownTest {
// 批量测试 WebSocket 查询
@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();
}
}
代码分析:
-
适用场景:适合验证任务是否全部完成,如小八超市的批量查询请求。
-
优势:简单易用,无需额外同步工具,适合任务数量较多但无需具体结果的场景。
-
注意点:
awaitTermination
需要在shutdown
后调用,且需设置合理超时时间。在小八超市场景中,可确保所有查询请求都发送完成。 -
局限性:无法直接获取任务结果,适合状态验证而非结果验证。
自定义 Runnable
以下是通用的 FunTesterRunnable
类,模拟耗时任务,用于测试验证:
package org.funtester.performance.books.chapter05.section7;
publicclass FunTesterRunnable implements Runnable {
privatefinallong start, end;
privatelong 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;
}
}
在 WebSocket 测试中,可将 FunTesterRunnable
替换为实际的查询任务。例如,任务可发送商品查询请求并验证响应内容,结合 MockServer 模拟服务端响应(如正常、库存不足、超时)。
总结
对基于 ExecutorService 的并发代码进行单元测试时,需要谨慎设计同步机制,以确保测试具有可靠性和确定性。依赖 Thread.sleep() 会导致测试不稳定并降低执行效率,应尽量避免。在本文中,我们展示了如何使用 CountDownLatch、Future 和 shutdown / awaitTermination() 等同步工具,替代传统的休眠方式,从而更加高效、稳健地测试并发逻辑。
最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】