2016-09-17 74 views
5

主题

我有一些代码,是决然不是线程安全的:测试线程安全的失败,斯波克

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()同时有望看到stringsnull,从而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(),如预计。

问题

为什么斯波克测试一致通过,我将如何去与斯波克测试呢?

+0

为什么有人想用代码的原始语言来编写测试代码?正如你自己发现这使得调试噩梦。除了语言可能继续前进以及如此受人喜爱的框架留下的事实之外。 –

+1

@OlegSklyar我发现Spock/Groovy是一个用简洁快捷的方式测试大多数Java代码的好工具。我认为这里的问题与Spock框架本身的实现有关,因为Groovy编译为Java字节码,并且使用Java调试器相当容易。另外,这是一个我在工作中遇到的问题的简单例子 - 我既不能抛弃Spock/Groovy也不能从Java迁移主代码库。 –

+0

不幸的是我没有你原来的问题的答案,所以对我来说这是一个哲学讨论......但是任何进一步的框架的介绍增加了复杂性。现在这种复杂性已经让你感到困扰。我相信当你决定重构代码时,美丽的框架也会重构所有的测试。当然它不会。虽然是自制的,但我不得不在Java项目中处理类似的框架:在编写Java测试库之前修复或修改测试是一场噩梦。现在所有的测试都是可以理解和重构的。 –

回答

4

你的问题的原因是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() 
})} 

当然,内斯波克它会使没有什么区别,只是在如何做到这一点,一般一个例子。

其次,并发测试的问题是他们从不保证你的程序是线程安全的。你所希望的最好的就是这样的测试会告诉你,你的程序已经坏了。但即使测试通过,也不能证明线程安全。为了增加查找并发错误的机会,您可能需要多次运行测试并收集统计信息。有时候,这种测试只会在数千次或数十万次运行中失败一次。你的课很简单,可以猜测它的线程安全性,但这种方法不适用于更复杂的情况。

+1

你,先生,是完全正确的。非常感谢你的解释!至于我的测试,我知道他们非常天真,可能会意外失败(或通过)“随机”。谢谢你的建议! –