2016-09-17 131 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

你,先生,是完全正確的。非常感謝你的解釋!至於我的測試,我知道他們非常天真,可能會意外失敗(或通過)「隨機」。謝謝你的建議! –