2016-09-17 14 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() एक साथ null रूप strings देखने के लिए उम्मीद कर रहे हैं, और इस तरह 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() पर कॉल करने के लिए होगा।

@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() लिए, के रूप में:

अजीब हिस्सा काफी कुछ समय बिताया डिबगिंग के बाद

निराश, मैं एक ही परीक्षण फिर से JUnit 4 और Mockito के साथ लिखा था अपेक्षित था।

सवाल

क्यों स्पॉक परीक्षण लगातार पारित होता है, और मैं स्पॉक के साथ इस परीक्षण के बारे में कैसे जाना होगा?

+0

कोड के लिए परीक्षा लिखने के लिए कोड की मूल भाषा की तुलना में कभी भी कुछ भी क्यों उपयोग करना चाहेंगे? जैसा कि आप स्वयं पाते हैं, यह एक दुःस्वप्न डीबगिंग करता है। इस तथ्य के अलावा कि भाषा आगे बढ़ सकती है और इतनी प्यारी ढांचा पीछे रहती है। –

+1

@OlegSklyar मुझे स्पार्क/ग्रोवी को अधिकांश जावा कोड को टेर्स और त्वरित तरीके से परीक्षण करने के लिए एक शानदार टूल मिल गया है। मुझे लगता है कि समस्या यहां स्पॉक फ्रेमवर्क के कार्यान्वयन के साथ है, क्योंकि ग्रोवी जावा बाइटकोड में संकलित है और इसके साथ जावा डीबगर का उपयोग करना काफी आसान है। साथ ही, यह एक समस्या की स्थिति में सामने आने वाली किसी समस्या का एक संक्षिप्त उदाहरण है- मैं न तो स्पॉक/ग्रोवी को खो सकता हूं और न ही जावा से मुख्य कोड बेस माइग्रेट कर सकता हूं। –

+0

दुर्भाग्यवश मेरे पास आपके मूल प्रश्न का उत्तर नहीं है, इसलिए मेरे लिए यह एक दार्शनिक चर्चा है ... लेकिन किसी और ढांचे की शुरूआत जटिलता को बढ़ाती है। उस जटिलता ने अब आपको काट दिया है। मेरा मानना ​​है कि जब आप कोड को दोबारा करने का फैसला करते हैं तो सुंदर ढांचा सभी परीक्षणों को भी दोबारा कर देगा। निश्चित रूप से यह नहीं होगा। हालांकि स्वयं निर्मित किए गए मुझे जावा परियोजनाओं में से एक में समान ढांचे से निपटना पड़ा: जब तक हमने जावा टेस्ट-बेस लाइब्रेरी नहीं लिखी, तब तक परीक्षणों को ठीक करने या संशोधित करने के लिए दुःस्वप्न था। अब सभी परीक्षण समझदार और अपवर्तक हैं। –

उत्तर

4

आपकी समस्या का कारण यह है कि स्पॉक सिंक्रनाइज़ किए गए तरीकों को बनाता है। विशेष रूप से, विधि 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; 
} 

इस तरह से स्पॉक अनजाने में आपकी समरूपता समस्या को हल करता है। मॉकिटो स्पष्ट रूप से एक और दृष्टिकोण का उपयोग करता है।

अपने परीक्षणों पर अन्य विचारों के एक जोड़े:

सबसे पहले, आप सुनिश्चित करें कि आपके सभी थ्रेड एक ही पल में 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

आप, महोदय, पूरी तरह से सही हैं। इस स्पष्टीकरण के लिए बहुत बहुत धन्यवाद! मेरे परीक्षणों के लिए, मुझे पता है कि वे बेहद बेवकूफ़ हैं और गलती से असफल हो सकते हैं (या पास) "यादृच्छिक रूप से"। आपका सुझाव देने के लिए धन्यवाद! –

संबंधित मुद्दे