测试线程安全的失败,斯波克
主题
我有一些代码,是决然不是线程安全的:测试线程安全的失败,斯波克
public class ExampleLoader
{
private List<String> strings;
protected List<String> loadStrings()
{
return Arrays.asList("Hello", "World", "Sup");
}
public List<String> getStrings()
{
if (strings == null)
{
strings = loadStrings();
}
return strings;
}
}
多个线程访问getStrings()
同时有望看到strings
为null
,从而loadStrings()
(这是一项昂贵的操作)被多次触发。
我想使代码线程安全的,而作为世界的一个好公民,我写了一个失败的斯波克规范第一问题:
def "getStrings is thread safe"() {
given:
def loader = Spy(ExampleLoader)
def threads = (0..<10).collect { new Thread({ loader.getStrings() })}
when:
threads.each { it.start() }
threads.each { it.join() }
then:
1 * loader.loadStrings()
}
上面的代码创建并启动了10个线程每个呼叫getStrings()
。然后它声称loadStrings()
在所有线程完成时只被调用一次。
我预计这会失败。然而,它始终通过。什么?
经过涉及System.out.println
和其他无聊事情的调试会话后,我发现线程确实是异步的:它们的run()
方法以看似随机的顺序打印。然而,第一个线程访问getStrings()
会总是是只有线程调用loadStrings()
。
怪异的一部分相当长的一段时间花在调试完毕后
沮丧,我使用JUnit 4的Mockito再次写下了相同的测试:
@Test
public void getStringsIsThreadSafe() throws Exception
{
// given
ExampleLoader loader = Mockito.spy(ExampleLoader.class);
List<Thread> threads = IntStream.range(0, 10)
.mapToObj(index -> new Thread(loader::getStrings))
.collect(Collectors.toList());
// when
threads.forEach(Thread::start);
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// then
Mockito.verify(loader, Mockito.times(1))
.loadStrings();
}
这个测试持续失败,因为多次调用loadStrings()
,如预计。
问题
为什么斯波克测试一致通过,我将如何去与斯波克测试呢?
你的问题的原因是Spock使得它探测到的同步方法。特别是,所有这些呼叫通过的方法MockController.handle()
被同步。如果您将暂停和某些输出添加到getStrings()
方法中,您将很容易注意到它。
public List<String> getStrings() throws InterruptedException {
System.out.println(Thread.currentThread().getId() + " goes to sleep");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getId() + " awoke");
if (strings == null) {
strings = loadStrings();
}
return strings;
}
这样Spock无意中修复了并发问题。 Mockito显然使用另一种方法。
在你的测试一对夫妇的其他想法:
首先,你没有做很多工作来确保所有线程都来到getStrings()
呼叫在同一时刻,从而降低碰撞的概率。很长的时间可能会在线程之间传递(足够长的时间让第一个人在其他人启动之前完成调用)。 更好的方法是使用一些同步原语来消除线程启动时间的影响。例如,CountDownLatch
可能是使用的位置:
given:
def final CountDownLatch latch = new CountDownLatch(10)
def loader = Spy(ExampleLoader)
def threads = (0..<10).collect { new Thread({
latch.countDown()
latch.await()
loader.getStrings()
})}
当然,内斯波克它会使没有什么区别,只是在如何做到这一点,一般一个例子。
其次,并发测试的问题是他们从不保证你的程序是线程安全的。你所希望的最好的就是这样的测试会告诉你,你的程序已经坏了。但即使测试通过,也不能证明线程安全。为了增加查找并发错误的机会,您可能需要多次运行测试并收集统计信息。有时候,这种测试只会在数千次或数十万次运行中失败一次。你的课很简单,可以猜测它的线程安全性,但这种方法不适用于更复杂的情况。
你,先生,是完全正确的。非常感谢你的解释!至于我的测试,我知道他们非常天真,可能会意外失败(或通过)“随机”。谢谢你的建议! –
为什么有人想用代码的原始语言来编写测试代码?正如你自己发现这使得调试噩梦。除了语言可能继续前进以及如此受人喜爱的框架留下的事实之外。 –
@OlegSklyar我发现Spock/Groovy是一个用简洁快捷的方式测试大多数Java代码的好工具。我认为这里的问题与Spock框架本身的实现有关,因为Groovy编译为Java字节码,并且使用Java调试器相当容易。另外,这是一个我在工作中遇到的问题的简单例子 - 我既不能抛弃Spock/Groovy也不能从Java迁移主代码库。 –
不幸的是我没有你原来的问题的答案,所以对我来说这是一个哲学讨论......但是任何进一步的框架的介绍增加了复杂性。现在这种复杂性已经让你感到困扰。我相信当你决定重构代码时,美丽的框架也会重构所有的测试。当然它不会。虽然是自制的,但我不得不在Java项目中处理类似的框架:在编写Java测试库之前修复或修改测试是一场噩梦。现在所有的测试都是可以理解和重构的。 –