Spock 测试线程安全失败
Testing thread safety fails with Spock
主题
我有一些代码绝对不是线程安全的:
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()
(这是一个昂贵的操作)被多次触发。
问题
我想让代码线程安全,作为世界上的好公民,我先写了一个失败的 Spock 规范:
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 测试始终通过,我将如何使用 Spock 进行测试?
你的问题的原因是 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()
})}
当然,在 Spock 中它不会有任何区别,这只是一个关于如何进行一般操作的示例。
其次,并发测试的问题在于它们从不保证您的程序是线程安全的。您最多可以希望这样的测试会告诉您您的程序已损坏。但即使测试通过,也不能证明线程安全。为了增加发现并发错误的机会,您可能需要 运行 多次测试并收集统计信息。有时,此类测试仅在几千甚至几十万 运行 秒中才会失败一次。您的 class 足够简单,可以猜测其线程安全性,但这种方法不适用于更复杂的情况。
主题
我有一些代码绝对不是线程安全的:
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()
(这是一个昂贵的操作)被多次触发。
问题
我想让代码线程安全,作为世界上的好公民,我先写了一个失败的 Spock 规范:
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 测试始终通过,我将如何使用 Spock 进行测试?
你的问题的原因是 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()
})}
当然,在 Spock 中它不会有任何区别,这只是一个关于如何进行一般操作的示例。
其次,并发测试的问题在于它们从不保证您的程序是线程安全的。您最多可以希望这样的测试会告诉您您的程序已损坏。但即使测试通过,也不能证明线程安全。为了增加发现并发错误的机会,您可能需要 运行 多次测试并收集统计信息。有时,此类测试仅在几千甚至几十万 运行 秒中才会失败一次。您的 class 足够简单,可以猜测其线程安全性,但这种方法不适用于更复杂的情况。