2015-12-30 89 views
1

我想測試列表包含對象的實例。如何聲明一個List只包含一個特定類的一個實例?

例如,具有單個實例:

assertThat(mylist).containsExactly(Matchers.any(ExpectedType.class)); 

陣列從測試obj返回確實含有實例ExpectedType的正好一個對象。 但是我的測試失敗:

java.lang.AssertionError: Not true that <[[email protected]]> contains exactly <[an instance of ExpectedType]>. It is missing <[an instance of ExpectedType]> and has unexpected items <[[email protected]]>

我怎麼能寫這樣的測試?

回答

4

您正在嘗試編寫測試以查看List是否僅包含使用Hamcrest 的某個特定類的一個實例。相反,你應該用 Hamcrest 來寫這個測試。 Hamcrest和Truth都是用於使測試更具表現力的庫,每個都有其特定的用法,樣式和語法。如果你喜歡,你可以在你的測試中一起使用它們,但是如你所做的那樣將它們的方法鏈接在一起不會起作用。 (也許你感到困惑,因爲這兩個庫都可以擁有以assertThat開頭的斷言?)因此,對於這個特定的測試,您需要選擇其中一個並使用它。

兩個庫,但是,缺乏檢查一個List有一個且只有一個滿足條件項目的內置功能。因此,無論使用哪種庫,您都有兩種選擇:您可以對列表執行一些預處理,以便使用內置斷言,或者可以擴展庫的語言以賦予其功能。

下面是一個例子類演示了兩個庫兩個選項:

import com.google.common.collect.FluentIterable; 
import com.google.common.truth.*; 
import org.hamcrest.*; 
import org.junit.Test; 

import java.util.*; 

import static com.google.common.truth.Truth.assertAbout; 
import static com.google.common.truth.Truth.assert_; 
import static org.hamcrest.MatcherAssert.assertThat; 
import static org.hamcrest.Matchers.*; 

public class ExactlyOneInstanceTest { 
    List<Object> myList = Arrays.asList("", 3, 'A', new Object()); 

    @Test 
    public void hamcrestBuiltInTestExactlyOneInstance() { 
     long theNumberOfStringsInMyList = myList.stream().filter(o -> o instanceof String).count(); 
     assertThat(theNumberOfStringsInMyList, equalTo(1L)); 
    } 

    @Test 
    public void hamcrestExtendedTestExactlyOneInstance() { 
     assertThat(myList, HasExactlyOne.itemThat(is(instanceOf(String.class)))); 
    } 

    @Test 
    public void truthBuiltInTestExactlyOneInstance() { 
     long theNumberOfStringsInMyList = myList.stream().filter(o -> o instanceof String).count(); 
     // can't static import Truth.assertThat because of name clash, 
     // but we can use this alternative form 
     assert_().that(theNumberOfStringsInMyList).isEqualTo(1); 
    } 

    @Test 
    public void truthExtendedTestExactlyOneInstance() { 
     assertAbout(iterable()).that(myList).containsExactlyOneInstanceOf(String.class); 
    } 


    // Hamcrest custom matcher 
    static class HasExactlyOne<T> extends TypeSafeDiagnosingMatcher<Iterable<? super T>> { 
     Matcher<? super T> elementMatcher; 

     HasExactlyOne(Matcher<? super T> elementMatcher) { 
      this.elementMatcher = elementMatcher; 
     } 

     @Factory 
     public static <T> Matcher<Iterable<? super T>> itemThat(Matcher<? super T> itemMatcher) { 
      return new HasExactlyOne<>(itemMatcher); 
     } 

     @Override 
     public void describeTo(Description description) { 
      description 
       .appendText("a collection containing exactly one item that ") 
       .appendDescriptionOf(elementMatcher); 
     } 

     @Override 
     protected boolean matchesSafely(Iterable<? super T> item, Description mismatchDescription) { 
      return FluentIterable.from(item).filter(o -> elementMatcher.matches(o)).size() == 1; 
     } 
    } 

    // Truth custom extension 
    static <T> SubjectFactory<ExtendedIterableSubject<T>, Iterable<T>> iterable() { 
     return new SubjectFactory<ExtendedIterableSubject<T>, Iterable<T>>() { 
      @Override 
      public ExtendedIterableSubject<T> getSubject(FailureStrategy fs, Iterable<T> target) { 
       return new ExtendedIterableSubject<>(fs, target); 
      } 
     }; 
    } 

    static class ExtendedIterableSubject<T> extends IterableSubject<ExtendedIterableSubject<T>, T, Iterable<T>> { 
     ExtendedIterableSubject(FailureStrategy failureStrategy, Iterable<T> list) { 
      super(failureStrategy, list); 
     } 

     void containsExactlyOneInstanceOf(Class<?> clazz) { 
      if (FluentIterable.from(getSubject()).filter(clazz).size() != 1) { 
       fail("contains exactly one instance of", clazz.getName()); 
      } 
     } 
    } 
} 

嘗試運行,並找過那類,並可使用任何方式似乎是最自然的你。在編寫將來的測試時,可以嘗試使用可用的內置斷言,並嘗試使方法及其斷言的意圖立即可讀。如果您看到多次編寫相同的代碼,或者測試方法不太容易閱讀,請重構和/或擴展您使用的庫的語言。重複,直到一切都經過測試,所有的測試都容易理解。請享用!

+1

的問題是真實的只要'IterableSubject'上的Truth'contains'動詞檢查元素的相等性。而「Matcher」不是'ExpectedType'。感謝您的回答(最終接受)。 – rds

+0

請注意,您的'containsExactlyOneInstanceOf()'與['IterableSubject.containsExactly()'](https://google.github.io/truth/api/latest/com/google/common/truth/)行爲不同IterableSubject.html#containsExactly-java.lang.Object ...-) - 真理的方法需要完全給定的對象,沒有別的。 – dimo414

0

一個更簡單的解決方法是

for (Object elt : myList) { 
    assertThat(elt).isInstanceOf(ExpectedType.class); 
} 

heenenee's answer更優雅:

assertThat(iterable()).that(myList).containsInstancesOf(ExpectedType.class); 
0

首先,請注意IterableSubject.containsExactly()斷言輸入,這意味着 「正好包含所提供的對象或失敗」。 - 即使你可以在這裏傳遞Matcher對象 - 我們斷言該列表恰好包含一個ExpectedType實例。無論是現有的答案正確執行是不變的(而不是heenenee的方法,斷言的ExpectedType一個實例,任何數量的其他實例的,和你的解決方案斷言列表包含ExpectedType實例的任何數量的)。當我讀到你的問題時,你打算聲明一個確切的屬性,但不管這表明接受的解決方案存在問題 - 它可能會意外地導致你不打算做出的斷言。


當我碰上這樣的真理API的限制,第一件事我總是試圖簡單地分裂主張成單獨的步驟。這通常證明書寫容易,易於閱讀,並且通常可以防止錯誤。可以理解的是,人們經常試圖尋找優雅的單線隊員,但一般來說,連續斷言沒有任何問題。

很難在這裏擊敗策略:

assertThat(ls).hasSize(1); 
assertThat(Iterables.getOnlyElement(ls)).isInstanceOf(String.class); 

如果可迭代不是大小爲1,我們會得到一個錯誤告訴我們,(隨着迭代的內容)。如果是,我們斷言唯一的元素是String的一個實例。完成!


對於ň實例一般情況下,代碼也不可否認變得有點混亂,但它仍然是合理的。我們只是用assertWithMessage()包括有關在isInstanceOf()斷言名單的附加背景:

assertThat(ls).hasSize(n); 
for (int i = 0; i < ls.size(); i++) { 
    assertWithMessage("list: %s - index: %s", ls, i) 
     .that(ls.get(i)).isInstanceOf(String.class); 
} 

這既是更具可讀性和比實現自己的自定義Subject更清楚正確的。


由於Truth 0.29你可以做的更好使用 「Fuzzy Truth」 AKA Correspondence。這允許您基本上描述集合的一些轉換,然後斷言該轉換的結果。在這種情況下,我們將創建一個INSTANCEOF_CORRESPONDENCE

private static final Correspondence<Object, Class<?>> INSTANCEOF_CORRESPONDENCE = 
    new Correspondence<Object, Class<?>>() { 
     @Override 
     public boolean compare(@Nullable Object actual, @Nullable Class<?> expected) { 
     return expected.isInstance(actual); 
     } 

     @Override 
     public String toString() { 
     return "is instanceof"; 
     } 
    }; 

現在你可以一個漂亮的單行!

assertThat(ls).comparingElementsUsing(INSTANCEOF_CORRESPONDENCE) 
    .containsExactly(String.class); 

這種方法在自定義對象的一大好處是它更可擴展的 - 如果你決定做一個不同的斷言Correspondence實現並不需要改變,只是你的斷言:

assertThat(ls).comparingElementsUsing(INSTANCEOF_CORRESPONDENCE) 
    .doesNotContain(Integer.class); 

也有tentative plans支持方法引用和lambda表達式與.comparingElementsUsing(),這樣你就可以寫類似:

assertThat(ls).comparingElementsUsing((o, c) -> c.isInstance(o), "is instanceof") 
    .containsExactly(String.class); 
+0

我恭敬地不同意。單元測試應該努力做一個\ [[single](https://softwareengineering.stackexchange.com/questions/7823/is-it-ok-to-have-multiple-asserts-in-a-single-unit-test )\] \ [[assertion](http://xunitpatterns.com/Assertion%20Roulette.html)\]。此外,這裏的斷言或新的「函數」功能都不如自定義的「主題」提供的方法名稱更具可讀性。真相的文檔本身引導用戶轉向自定義主題,以使[[斷言儘可能接近自然語言。]](https://google.github.io/truth/extension) – heenenee

+0

我同意你引用的原則,然而,從那裏我們不應該爲每個可能的複雜斷言編寫自定義斷言邏輯。這不是關於單個測試有多少'assert'調用,而是關於正在創建的*概念*數量的斷言。一個測試應該在一個* fact *上聲明,這可能需要多個'assert'調用才能建立。 – dimo414

+0

隱藏自定義主題中的斷言複雜性不會改變正在執行的斷言的實際數量。而且,無論何時你創建一個自定義的'Subject'實現,爲該主題編寫測試*都是一個好主意*,否則你會爲靜默測試失敗引入一個新的向量。正如您的主題所證明的那樣,隨着不同的變體出現,您也很可能需要重新實現* n *個不同的斷言(包含,不包含,包含完全等等)。有了模糊真理,你可以免費獲得所有這些,你只需要寫一個小的函數即可。 – dimo414

相關問題