2016-07-24 17 views
1

我有一個計算某事的代碼,緩存是,如果已經計算,則從緩存中讀取;與此類似:在JUnit測試的情況下檢查類內的調用次數

public class LengthWithCache { 
    private java.util.Map<String, Integer> lengthPlusOneCache = new java.util.HashMap<String, Integer>(); 

    public int getLenghtPlusOne(String string) { 
     Integer cachedStringLenghtPlusOne = lengthPlusOneCache.get(string); 
     if (cachedStringLenghtPlusOne != null) { 
      return cachedStringLenghtPlusOne; 
     } 
     int stringLenghtPlusOne = determineLengthPlusOne(string); 
     lengthPlusOneCache.put(string, new Integer(stringLenghtPlusOne)); 
     return stringLenghtPlusOne; 
    } 

    protected int determineLengthPlusOne(String string) { 
     return string.length() + 1; 
    } 
} 

我希望測試功能determineLengthPlusOne已被調用的次數足夠多,這樣的:

public class LengthWithCacheTest { 
    @Test 
    public void testGetLenghtPlusOne() { 
     LengthWithCache lengthWithCache = new LengthWithCache(); 

     assertEquals(6, lengthWithCache.getLenghtPlusOne("apple")); 
     // here check that determineLengthPlusOne has been called once 

     assertEquals(6, lengthWithCache.getLenghtPlusOne("apple")); 
     // here check that determineLengthPlusOne has not been called 
    } 
} 

嘲諷類LengthWithCache似乎不是一個很好的選擇,因爲我想測試他們的功能。 (根據我的理解,我們嘲笑被測試的類使用的類,而不是被測試的類本身。)哪一個是最優雅的解決方案?

我的第一個想法是創建另一個類LengthPlusOneDeterminer包含函數determineLengthPlusOne,添加它傳遞給函數getLenghtPlusOne作爲參數,並在單元測試的情況下模擬LengthPlusOneDeterminer,但似乎有點奇怪,因爲它具有對工作造成不必要的影響代碼(類LengthWithCache的真實客戶端)。

基本上我使用的是Mockito,但無論是模擬框架(或其他解決方案),歡迎!謝謝!

+2

我認爲與嘲弄一個孤立的單元測試來測試這是不是最好的ID EA。相反,編寫一個測試,只驗證被測試類的*外部可觀察行爲*。內部使用緩存最終只是一個旨在提高性能的實現細節。也許可以寫一個測試來檢測這樣的性能改進(使用'System.nanoTime()');如果沒有,那麼緩存可能無用,可以刪除。 –

+0

謝謝你的評論。緩存機制在實踐中變得有用。 –

+1

爲什麼對這個問題倒下了?客觀地說,這是一個非常有趣的混合設計和單元測試主題。它值得更好。 – davidxxx

回答

1

感覺就像使用嘲笑正在推翻它。 LengthWithCache可以在測試的上下文中作爲匿名內部類來重寫,以獲得調用計數。這不需要對正在測試的現有班級進行重組。

public class LengthWithCacheTest { 
    @Test 
    public void verifyLengthEval() { 
     LengthWithCache lengthWithCache = new LengthWithCache(); 
     assertEquals(6, lengthWithCache.getLenghtPlusOne("apple")); 
    } 

    @Test 
    public void verifyInvocationCounts() { 
     final AtomicInteger plusOneInvkCounter = new AtomicInteger(); 
     LengthWithCache lengthWithCache = new LengthWithCache() { 
      @Override 
      protected int determineLengthPlusOne(String string) { 
       plusOneInvkCounter.incrementAndGet(); 
       return super.determineLengthPlusOne(string); 
      } 
     }; 

      lengthWithCache.getLenghtPlusOne("apple"); 
      assertEquals(1, plusOneInvkCounter.get()); 

      lengthWithCache.getLenghtPlusOne("apple"); 
      lengthWithCache.getLenghtPlusOne("apple"); 
      lengthWithCache.getLenghtPlusOne("apple"); 
      lengthWithCache.getLenghtPlusOne("apple"); 
      lengthWithCache.getLenghtPlusOne("apple"); 
      lengthWithCache.getLenghtPlusOne("apple"); 

      assertEquals(1, plusOneInvkCounter.get()); 
    } 
} 

值得注意的兩次測試之間的間隔。一個驗證 長度eval是正確的,另一個驗證調用 計數。


如果需要用於驗證的更寬的數據集,則可以轉動測試上述成參數測試和提供多個數據集和期望。在下面的示例中,我添加了50個字符串(長度1-50)的數據集,一個空字符串和一個空值。 空失敗

@RunWith(Parameterized.class) 
    public class LengthWithCacheTest { 
     @Parameters(name="{0}") 
     public static Collection<Object[]> buildTests() { 
      Collection<Object[]> paramRefs = new ArrayList<Object[]>(); 
      paramRefs.add(new Object[]{null, 0}); 
      paramRefs.add(new Object[]{"", 1}); 
      for (int counter = 1 ; counter < 50; counter++) { 
       String data = ""; 
       for (int index = 0 ; index < counter ; index++){ 
        data += "a"; 
       } 
       paramRefs.add(new Object[]{data, counter+1}); 
      } 

      return paramRefs; 
     } 

     private String stringToTest; 
     private int expectedLength; 

     public LengthWithCacheTest(String string, int length) { 
      this.stringToTest = string; 
      this.expectedLength = length; 
     } 



    @Test 
    public void verifyLengthEval() { 
     LengthWithCache lengthWithCache = new LengthWithCache(); 
     assertEquals(expectedLength, lengthWithCache.getLenghtPlusOne(stringToTest)); 
    } 

    @Test 
    public void verifyInvocationCounts() { 
     final AtomicInteger plusOneInvkCounter = new AtomicInteger(); 
     LengthWithCache lengthWithCache = new LengthWithCache() { 
      @Override 
      protected int determineLengthPlusOne(String string) { 
       plusOneInvkCounter.incrementAndGet(); 
       return super.determineLengthPlusOne(string); 
      } 
     }; 

      assertEquals(0, plusOneInvkCounter.get()); 
      lengthWithCache.getLenghtPlusOne(stringToTest); 
      assertEquals(1, plusOneInvkCounter.get()); 
      lengthWithCache.getLenghtPlusOne(stringToTest); 
      assertEquals(1, plusOneInvkCounter.get()); 
      lengthWithCache.getLenghtPlusOne(stringToTest); 
      assertEquals(1, plusOneInvkCounter.get()); 

    } 
} 

參數測試是改變你的數據通過測試設置的最佳途徑之一,但它增加了複雜性的考驗,可能難以維持。瞭解這個工作很有用,但並不總是正確的工具。

+0

謝謝;這種方法與davidhxxx提供的方法類似,但更簡單。 –

+0

該方法更簡單,因爲它不處理副作用。它假定始終使用相同的字符串值。完整的單元測試必須檢查副作用,而不僅僅是簡單和簡單的情況。因爲問題通常來自棘手的情況。 我會編輯我的答案以顯示它。 – davidxxx

2

最優雅的方法是創建一個單獨的類,它執行緩存並使用當前類進行修飾(在刪除緩存後),這樣您可以安全地單元測試緩存本身,而不會干擾基底的功能類。

public class Length { 
    public int getLenghtPlusOne(String string) { 
     int stringLenghtPlusOne = determineLengthPlusOne(string); 
     lengthPlusOneCache.put(string, new Integer(stringLenghtPlusOne)); 
     return stringLenghtPlusOne; 
    } 

    protected int determineLengthPlusOne(String string) { 
     return string.length() + 1; 
    } 
} 

public class CachedLength extends Length { 
    private java.util.Map<String, Integer> lengthPlusOneCache = new java.util.HashMap<String, Integer>(); 

    public CachedLength(Length length) { 
     this.length = length; 
    } 

    public int getLenghtPlusOne(String string) { 
     Integer cachedStringLenghtPlusOne = lengthPlusOneCache.get(string); 
     if (cachedStringLenghtPlusOne != null) { 
      return cachedStringLenghtPlusOne; 
     } 
     return length.getLenghtPlusOne(string); 
    } 
} 

然後你就可以輕鬆地測試緩存我的注入嘲笑Length

Length length = Mockito.mock(Length.class); 
CachedLength cached = new CachedLength(length); 
.... 
Mockito.verify(length, Mockito.times(5)).getLenghtPlusOne(Mockito.anyInt()); 
+0

感謝您的建議!事實上,這不是一個語法完全正確的解決方案,但方法很好。 我認爲沒有必要從長度繼承,但使用它的屬性。 –

2

你不需要模擬滿足您的需要。 要測試內部行爲(被調用或未調用getLenghtPlusOne()),您需要有一個方法來訪問LengthWithCache中的緩存。
但是,在您的設計級別,我們想象您不想以公開的方式打開緩存。這是正常的。
存在多種解決方案來對緩存行爲進行測試,儘管存在此限制。 我將介紹我的做法。也許,有更好的。 但我認爲在大多數情況下,您將被迫使用一些技巧或將您的設計複雜化以進行單元測試。

它依靠擴展您的課程來測試,通過擴展它來爲您的測試添加所需的信息和行爲。
它是你將在你的單元測試中使用的這個子類。
此類擴展中最重要的一點不是要中斷或修改要測試的對象的行爲。
它必須添加新信息並添加新行爲,而不是修改原始類的信息和行爲,否則測試會失去它的價值,因爲它不再測試原始類中的行爲。

要點:
- 具有註冊了當前呼叫,如果該方法lengthPlusOneWasCalled被稱爲
私有字段lengthPlusOneWasCalledForCurrentCall - 有一個公共的方法來知道lengthPlusOneWasCalledForCurrentCall對作爲參數字符串值。它啓用了斷言。
- 有一個公共方法來清除lengthPlusOneWasCalledForCurrentCall的狀態。它能夠在斷言後保持乾淨的狀態。

package cache; 

import java.util.HashSet; 
import java.util.Set; 

import org.junit.Assert; 
import org.junit.Test; 

public class LengthWithCacheTest { 

    private class LengthWithCacheAugmentedForTest extends LengthWithCache { 

     private Set<String> lengthPlusOneWasCalledForCurrentCall = new HashSet<>(); 

     @Override 
     protected int determineLengthPlusOne(String string) { 
      // start : info for testing 
      this.lengthPlusOneWasCalledForCurrentCall.add(string); 
      // end : info for testing 
      return super.determineLengthPlusOne(string); 
     } 

     // method for assertion 
     public boolean isLengthPlusOneCalled(String string) { 
      return lengthPlusOneWasCalledForCurrentCall.contains(string); 
     } 

     // method added for clean the state of current calls 
     public void cleanCurrentCalls() { 
      lengthPlusOneWasCalledForCurrentCall.clear(); 
     } 

    } 

    @Test 
    public void testGetLenghtPlusOne() { 
    LengthWithCacheAugmentedForTest lengthWithCache = new LengthWithCacheAugmentedForTest(); 

     final String string = "apple"; 
     // here check that determineLengthPlusOne has been called once 
     Assert.assertEquals(6, lengthWithCache.getLenghtPlusOne(string)); 
     Assert.assertTrue(lengthWithCache.isLengthPlusOneCalled(string)); 

     // clean call registered 
     lengthWithCache.cleanCurrentCalls(); 

     // here check that determineLengthPlusOne has not been called 
     Assert.assertEquals(6, lengthWithCache.getLenghtPlusOne(string)); 
     Assert.assertFalse(lengthWithCache.isLengthPlusOneCalled(string)); 
    }      
} 

編輯28-07-16說明爲什麼需要更多的代碼來處理更多的場景

想,我會通過聲稱有沒有副作用提高了測試:在添加元素密鑰的緩存對於其他密鑰的緩存處理方式沒有影響。 此測試失敗,因爲它不依賴於字符串鍵。所以,它總是增加。

@Test 
    public void verifyInvocationCountsWithDifferentElementsAdded() { 
    final AtomicInteger plusOneInvkCounter = new AtomicInteger(); 
    LengthWithCache lengthWithCache = new LengthWithCache() { 
     @Override 
     protected int determineLengthPlusOne(String string) { 
      plusOneInvkCounter.incrementAndGet(); 
      return super.determineLengthPlusOne(string); 
      } 
     }; 

    Assert.assertEquals(0, plusOneInvkCounter.get()); 
    lengthWithCache.getLenghtPlusOne("apple"); 
    Assert.assertEquals(1, plusOneInvkCounter.get()); 
    lengthWithCache.getLenghtPlusOne("pie"); 
    Assert.assertEquals(1, plusOneInvkCounter.get()); 
    lengthWithCache.getLenghtPlusOne("eggs"); 
    Assert.assertEquals(1, plusOneInvkCounter.get());   
    } 

我的版本更長,因爲它提供了更多功能,因此它可以處理更廣泛的單元測試場景。

編輯28-07-16點整數緩存

與原來的問題,但很少眨眼:) 你getLenghtPlusOne(String string)應該使用Integer.valueOf(int)代替new Integer(int) Integer.valueOf(int)使用內部緩存

沒有直接關係
+0

感謝您的建議!我也認爲(在另一種情況下,我使用了類似的方法),但是我的基本問題是單元測試非常依賴於要測試的類的內部細節。無論基類的小修改都應立即反映在相關的單元測試類中,因此很難翻新基類。這是一個可行的解決方案,但我認爲它不是很優雅。 –

+0

不客氣。我喜歡單元測試。它更長,但有充分的理由。閱讀我的編輯和我對耶利米回答的評論。 – davidxxx

0

基於krzyk的建議,這裏是我完全可行的解決方案:

計算器本身:

public class LengthPlusOneCalculator { 
    public int calculateLengthPlusOne(String string) { 
     return string.length() + 1; 
    } 
} 

單獨高速緩存機制:

public class LengthPlusOneCache { 
    private LengthPlusOneCalculator lengthPlusOneCalculator; 
    private java.util.Map<String, Integer> lengthPlusOneCache = new java.util.HashMap<String, Integer>(); 

    public LengthPlusOneCache(LengthPlusOneCalculator lengthPlusOneCalculator) { 
     this.lengthPlusOneCalculator = lengthPlusOneCalculator; 
    } 

    public int calculateLenghtPlusOne(String string) { 
     Integer cachedStringLenghtPlusOne = lengthPlusOneCache.get(string); 
     if (cachedStringLenghtPlusOne != null) { 
      return cachedStringLenghtPlusOne; 
     } 
     int stringLenghtPlusOne = lengthPlusOneCalculator.calculateLengthPlusOne(string); 
     lengthPlusOneCache.put(string, new Integer(stringLenghtPlusOne)); 
     return stringLenghtPlusOne; 
    } 

} 

用於檢查LengthPlusOneCalculator單元測試:

import static org.junit.Assert.assertEquals; 
import org.junit.Test; 

public class LengthPlusOneCalculatorTest { 
    @Test 
    public void testCalculateLengthPlusOne() { 
     LengthPlusOneCalculator lengthPlusOneCalculator = new LengthPlusOneCalculator(); 
     assertEquals(6, lengthPlusOneCalculator.calculateLengthPlusOne("apple")); 
    } 

} 

最後,單元測試用於LengthPlusOneCache,檢查調用次數:

import static org.junit.Assert.assertEquals; 
import static org.mockito.Mockito.*; 
import org.junit.Test; 

public class LengthPlusOneCacheTest { 
    @Test 
    public void testNumberOfInvocations() { 
     LengthPlusOneCalculator lengthPlusOneCalculatorMock = mock(LengthPlusOneCalculator.class); 
     when(lengthPlusOneCalculatorMock.calculateLengthPlusOne("apple")).thenReturn(6); 
     LengthPlusOneCache lengthPlusOneCache = new LengthPlusOneCache(lengthPlusOneCalculatorMock); 

     verify(lengthPlusOneCalculatorMock, times(0)).calculateLengthPlusOne("apple"); // verify that not called yet 
     assertEquals(6, lengthPlusOneCache.calculateLenghtPlusOne("apple")); 
     verify(lengthPlusOneCalculatorMock, times(1)).calculateLengthPlusOne("apple"); // verify that already called once 
     assertEquals(6, lengthPlusOneCache.calculateLenghtPlusOne("apple")); 
     verify(lengthPlusOneCalculatorMock, times(1)).calculateLengthPlusOne("apple"); // verify that not called again 
    } 
} 

我們可以安全地執行嘲笑機制,因爲我們已經確信嘲笑的類可以正常工作,使用它自己的單元測試。

通常這是內置於構建系統;這個例子可編譯和運行命令行如下(文件junit-4.10.jarmockito-all-1.9.5.jar必須複製到工作目錄):

javac -cp .;junit-4.10.jar;mockito-all-1.9.5.jar *.java 
java -cp .;junit-4.10.jar org.junit.runner.JUnitCore LengthPlusOneCalculatorTest 
java -cp .;junit-4.10.jar;mockito-all-1.9.5.jar org.junit.runner.JUnitCore LengthPlusOneCacheTest 

不過,我仍然不完全滿意這種做法。我的問題如下:

  1. 功能calculateLengthPlusOne被嘲笑。我更喜歡這樣的解決方案,其中一個模擬或任何框架只計算調用次數,但原始代碼運行。 (不知何故,由davidhxxx提到,但我不覺得這是一個完美的。)

  2. 該代碼變得有點過於複雜。這不是人們通常創造的方式。因此,如果原始代碼不是我們的完全控制,這種方法是不夠的。這可能是現實中的一個限制。

  3. 通常我會使功能calculateLengthPlusOne靜態。這種方法在這種情況下不起作用。 (但也許我的Mockito知識很薄弱。)

如果有人能解決任何這些問題,我會非常感激!

+1

JMockit嘲笑庫能夠滿足上述所有問題1,2和3。這就是說,我個人仍然覺得這是最好的測試,沒有任何嘲諷。瞭解實際的真實世界緩存是如何使用的,這是非常有用的,因爲這裏顯示的例子太過於人爲設計了。 –

+0

是的,似乎是這樣;耶利米的解決方案確實是一個簡單而偉大的解決方案。的確,我在這裏留下我的解決方案,至少是因爲語法。 –

+0

真實世界的例子是計算源代碼元素的兩個完全限定名稱之間的距離,如文章https://dibt.unimol.it/staff/fpalomba/documents/C8.pdf中所述。這是非常頻繁,令人驚訝的慢。在實踐中有幾百個源代碼元素;例如,我們假設300.我創建一個300x300數組,並保存已經計算出的距離值。下次我只需要讀出已保存的值。 –

1

由於這是一個有趣的問題,我決定寫測試。有兩種不同的方式,一種是嘲諷而另一種沒有。 (個人而言,我更喜歡的版本不嘲笑。)在任一種情況下,原來的類被測試時,沒有修改:

package example; 

import mockit.*; 
import org.junit.*; 
import static org.junit.Assert.*; 

public class LengthWithCacheMockedTest { 
    @Tested(availableDuringSetup = true) @Mocked LengthWithCache lengthWithCache; 

    @Before 
    public void recordComputedLengthPlusOneWhileFixingTheNumberOfAllowedInvocations() { 
     new Expectations() {{ 
      lengthWithCache.determineLengthPlusOne(anyString); result = 6; times = 1; 
     }}; 
    } 

    @Test 
    public void getLenghtPlusOneNotFromCacheWhenCalledTheFirstTime() { 
     int length = lengthWithCache.getLenghtPlusOne("apple"); 

     assertEquals(6, length); 
    } 

    @Test 
    public void getLenghtPlusOneFromCacheWhenCalledAfterFirstTime() { 
     int length1 = lengthWithCache.getLenghtPlusOne("apple"); 
     int length2 = lengthWithCache.getLenghtPlusOne("apple"); 

     assertEquals(6, length1); 
     assertEquals(length1, length2); 
    } 
} 

package example; 

import mockit.*; 
import org.junit.*; 
import static org.junit.Assert.*; 

public class LengthWithCacheNotMockedTest { 
    @Tested LengthWithCache lengthWithCache; 

    @Test 
    public void getLenghtPlusOneNotFromCacheWhenCalledTheFirstTime() { 
     long t0 = System.currentTimeMillis(); // millisecond precision is enough here 
     int length = lengthWithCache.getLenghtPlusOne("apple"); 
     long dt = System.currentTimeMillis() - t0; 

     assertEquals(6, length); 
     assertTrue(dt >= 100); // assume at least 100 millis to compute the expensive value 
    } 

    @Test 
    public void getLenghtPlusOneFromCacheWhenCalledAfterFirstTime() { 
     // First time: takes some time to compute. 
     int length1 = lengthWithCache.getLenghtPlusOne("apple"); 

     // Second time: gets from cache, takes no time. 
     long t0 = System.nanoTime(); // max precision here 
     int length2 = lengthWithCache.getLenghtPlusOne("apple"); 
     long dt = System.nanoTime() - t0; 

     assertEquals(6, length1); 
     assertEquals(length1, length2); 
     assertTrue(dt < 1000000); // 1000000 nanos = 1 millis 
    } 
} 

僅有一個細節:爲測試上述工作,我加入LengthWithCache#determineLengthPlusOne(String)方法內以下行,以模擬真實世界的場景,其中的計算需要一些時間:

try { Thread.sleep(100); } catch (InterruptedException ignore) {} 
+0

感謝您提供的代碼;檢查時間是一個有趣的想法。 –

相關問題