2015-12-30 57 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

相关问题