使用Spock测试线程安全性失败

Mar*_*ann 7 java testing multithreading unit-testing 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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()
}
Run Code Online (Sandbox Code Playgroud)

上面的代码创建并启动每个调用的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();
}
Run Code Online (Sandbox Code Playgroud)

loadStrings()正如预期的那样,由于多次调用,此测试始终失败.

这个问题

为什么Spock测试一直通过,我将如何使用Spock进行测试?

And*_*gin 8

你的问题的原因是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;
}
Run Code Online (Sandbox Code Playgroud)

这样Spock就会无意中修复你的并发问题.Mockito显然使用另一种方法.

关于测试的其他一些想法:

首先,您没有做太多工作来确保所有线程都getStrings()在同一时刻进入呼叫,从而降低了冲突的可能性.长时间可以在线程开始之间传递(足够长,第一个完成调用,然后其他人启动它).更好的方法是使用一些同步原语来消除线程启动时间的影响.例如,a 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()
})}
Run Code Online (Sandbox Code Playgroud)

当然,在Spock中它没有任何区别,它只是一般如何做到这一点的一个例子.

其次,并发性测试的问题在于它们永远不能保证您的程序是线程安全的.您可以期待的最好的是,此类测试将向您显示您的程序已损坏.但即使测试通过,它也不能证明线程安全.为了增加查找并发错误的机会,您可能需要多次运行测试并收集统计信息.有时候,这样的测试只会在数千次甚至数十万次运行中失败一次.你的类很简单,可以猜测它的线程安全性,但这种方法不适用于更复杂的情况.

  • 先生,你是完全正确的.非常感谢您的解释!至于我的测试,我知道他们非常幼稚,可能会意外地"随机"失败(或传递).谢谢你的建议! (2认同)