子线程执行了断言,预期 1,实际 0。终端已经打印了这两个数字,JUnit 却给出 1 个测试通过。后来我保持这个失败输入,只让测试线程读取 Future,结果变成了 1 个测试失败。
先把失败传回测试线程
ExecutorService.submit 返回的 Future 会保存任务结果。任务抛出的异常也在里面。只调用 awaitTermination,可以确认线程池结束,但它不会把某个任务里的异常抛给 JUnit。
这次对照跑了三次。第一次让工作线程断言 0 等于 1,然后丢弃 Future。第二次还是 0,增加带 5 秒超时的 get。第三次保留同样的检查,把模拟写入的返回值改成 1。

下面是三份控制台输出的关键行。第二次的退出码 1 是这次故意保留的失败。
text
IgnoredFailureTest (exit 0)
worker: expected=1 actual=0
[ 1 tests successful ]
[ 0 tests failed ]
ObservedFailureTest (exit 1)
worker: expected=1 actual=0
=> java.util.concurrent.ExecutionException: org.opentest4j.AssertionFailedError: expected: <1> but was: <0>
[ 0 tests successful ]
[ 1 tests failed ]
FixedWorkerTest (exit 0)
worker: expected=1 actual=1
[ 1 tests successful ]
[ 0 tests failed ]
Future.get 抛出的 ExecutionException 包着子线程的断言错误。如果测试主动捕获它,应该继续检查或重新抛出异常。只打一行日志然后返回,会再丢一次失败。
运行后把三份记录留住
环境为 JDK 21 和 JUnit Platform Console Standalone 1.12.2。保存下面的完整文件为 AsyncRegressionTest.java。三个测试类独立运行,便于记录旧版漏检、修正测试后失败、正确输入通过。命令会把 JUnit 的报告分目录写出,控制台输出与退出码也一并保留。
powershell
$jar = '.\junit-platform-console-standalone-1.12.2.jar'
javac -encoding UTF-8 -cp $jar AsyncRegressionTest.java
if ($LASTEXITCODE -ne 0) { throw 'compile failed' }
$classes = @('IgnoredFailureTest','ObservedFailureTest','FixedWorkerTest')
$results = foreach ($class in $classes) {
$argsForTest = @('execute','--class-path','.','--select-class',$class,
'--disable-ansi-colors','--reports-dir',("runs/" + $class))
java -jar $jar @argsForTest *> ("run-" + $class + ".txt")
"$class exit=$LASTEXITCODE"
}
$results | Set-Content -Encoding utf8 exit-codes.txt
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class AsyncRegressionTest {
static Future<?> verifyWrite(ExecutorService pool, int actualRows) {
return pool.submit(() -> {
System.out.printf("worker: expected=1 actual=%d%n", actualRows);
assertEquals(1, actualRows);
});
}
}
class IgnoredFailureTest {
@Test
void workerEndsButFailureIsIgnored() throws InterruptedException {
try (var pool = Executors.newSingleThreadExecutor()) {
// 模拟写入结果为 0,任务里的断言会失败。
AsyncRegressionTest.verifyWrite(pool, 0);
pool.shutdown();
assertTrue(pool.awaitTermination(5, TimeUnit.SECONDS));
}
}
}
class ObservedFailureTest {
@Test
void getPropagatesWorkerFailure() throws Exception {
try (var pool = Executors.newSingleThreadExecutor()) {
Future<?> job = AsyncRegressionTest.verifyWrite(pool, 0);
job.get(5, TimeUnit.SECONDS);
}
}
}
class FixedWorkerTest {
@Test
void workerResultIsVerified() throws Exception {
try (var pool = Executors.newSingleThreadExecutor()) {
// 只改变模拟返回值,继续用相同断言与等待方式。
Future<?> job = AsyncRegressionTest.verifyWrite(pool, 1);
job.get(5, TimeUnit.SECONDS);
}
}
}
测试文件中的写入行数是固定输入。这次只验证子线程的断言能否影响测试结果。接入真实数据库后,要在任务完成并提交后再查落库数据。多个 Future 对应多个需要验收的任务时,逐个收集结果,保留能定位失败任务的标识。
你们的异步测试会故意让一个任务失败跑一遍吗?最容易漏掉的是执行异常、等待超时,还是任务根本没开始?
有关 Future 的结果和异常,见 JDK 21 Future 文档。控制台命令参考 JUnit 5.12.2 用户指南。