测试多线程代码是一个艰巨的挑战。 尝试测试并发性时获得的第一个建议是尽可能地在代码中隔离并发问题。 这是一般的设计建议,但在这种情况下甚至更重要。 确保首先正确地对并发构造所包装的逻辑进行单元测试。 否则,您可能会花费很长时间尝试找出一个并发问题,最终发现这是有缺陷的业务逻辑。
了解了这些内容之后,您就可以考虑测试并发系统的策略。 GOOS涵盖了您如何做到的。 在这里,您可以找到我将要解释的代码:
首先,让我们看一下被测系统:
public class AtomicBigCounter {
private BigInteger count = BigInteger.ZERO;
public BigInteger count() {
return count;
}
public void inc() {
count = count.add(BigInteger.ONE);
}
}
如您所见,该类不是线程安全的,因为它通过inc()方法公开了一些状态。 状态不是线程安全的(您可以使用AtomicInteger而不是BigInteger进行修复)。 为了测试该类,我们将包括一个非并行和并行测试。
@Test public void canIncreaseCounter(){
...
}
@Test public void canIncrementCounterFromMultipleThreadsSimultaneously()
throws InterruptedException {
MultithreadedStressTester stressTester = new MultithreadedStressTester(25000);
stressTester.stress(new Runnable() {
public void run() {
counter.inc();
}
});
stressTester.shutdown();
assertThat("final count", counter.count(),
equalTo(BigInteger.valueOf(stressTester.totalActionCount())));
}
压力测试仪将使用m个线程执行n个循环的方法。 随着我们的方法递增1,我们应该看到n*m
等于counter.count()
。
有趣的类是MultithreadedStressTester:
public void stress(final Runnable action) throws InterruptedException {
spawnThreads(action).await();
}
private CountDownLatch spawnThreads(final Runnable action) {
final CountDownLatch finished = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executor.execute(new Runnable() {
public void run() {
try {
repeat(action);
}
finally {
finished.countDown();
}
}
});
}
return finished;
}
private void repeat(Runnable action) {
for (int i = 0; i < iterationCount; i++) {
action.run();
}
}
如果执行该测试,您将收到不同的结果,有时甚至会通过! 那是因为该测试不是确定性的,所以我们无法确保线程在每次执行中如何交错。 如果我们希望尽可能确保此测试找到可能的错误,则应增加线程和迭代的数量,但要权衡时间。
您可以使用Weaver使用更具确定性的方法。 为了理解它是如何工作的,让我们用一个例子来说明它。 假设我们有一个内存中而不是线程安全的存储:
private final Map<Level, Scores> scoresByLevel;
我们有一些服务可以访问包装该存储库的存储库:
1 Optional<Scores> scoresFromStore = scoreRepo.findBy(score.level());
2 if(scoresFromStore.isPresent()) {
3 scoreRepo.update(score.level(), score);
4 } else {
5 scoreRepo.save(score.level(), new Scores().add(score));
6 }
该服务位于服务器中,是一个单例,每个请求都会生成一个线程,因此我们希望自动执行该部分。 我们可以使用压力测试非确定性方法,也可以使用Weaver。 如果我们对这个问题进行深入思考,我们就会意识到我们要测试以下各项的每个组合(例如,线程1在x时刻执行第1行,线程2在x时刻执行第1行,将是-> T1 / 1: T2 / 1)
- T1 / 1:T2 / 1
- T1 / 1:T2 / 2
- T1 / 1:T2 / 3
- …。
- T1 / 2:T2 / 1
- T1 / 2:T2 / 2
- T1 / 2:T2 / 3
- …。
例如,如果T1 / 5和T2 / 2尚未保存,而T2已经从商店中获得了空分,那么我们将遇到问题。 这意味着T1将得分保存在一个级别中,然后T2将这样做,从而破坏了逻辑。 这正是Weaver所做的,它获取一个方法并使用两个线程执行上述组合。
如果我删除了准备代码(用@ThreadedBefore注释),则测试代码将如下所示:
@ThreadedMain
public void mainThread() {
scoreService.save(LEVEL_ID, SCORE_VALUE, aUser);
}
@ThreadedSecondary
public void secondThread() {
scoreService.save(LEVEL_ID, ANOTHER_SCORE_VALUE, aUser);
}
@ThreadedAfter
public void after() {
Optional<Scores> scores = scoreRepo.findBy(aLevel());
assertThat(scores.isPresent()).isTrue();
assertThat(scores.get().contains(aScoreWith(aUser))).isTrue();
assertThat(scores.get().contains(aDifferentScoreWith(aUser))).isTrue();
}
@Test
public void testThreading() {
new AnnotatedTestRunner().runTests(this.getClass(), ScoreService.class);
}
由于确定性,该测试将始终失败。 如您所见,测试并发性非常困难,这就是为什么我支持现代框架的原因,这些框架试图将麻烦隐藏在平台中或通过不可变数据解决问题。
- =