如何最好地对集合进行单元测试?

23

我想知道人们如何对“期望”的集合进行单元测试和断言,以确保它与“实际”集合相同/类似(顺序不重要)。

为了执行这个断言,我编写了一个简单的assert API:

public void assertCollection(Collection<?> expectedCollection, Collection<?> actualCollection) {
    assertNotNull(expectedCollection);
    assertNotNull(actualCollection);
    assertEquals(expectedCollection.size(), actualCollection.size());
    assertTrue(expectedCollection.containsAll(actualCollection));
    assertTrue(actualCollection.containsAll(expectedCollection));
}

它有效。如果我只断言一堆整数或字符串,那么这很简单。但是如果我要断言一组Hibernate域,那么这可能会非常麻烦。例如,collection.containsAll(..)依赖于equals(..)执行检查,但我总是覆盖我的Hibernate域中的equals(..),只检查业务键(这是Hibernate网站上规定的最佳实践),而不是该域的所有字段。当然,只检查业务键是有意义的,但有时我确实希望确保所有字段都正确,而不仅仅是业务键(例如,新的数据输入记录)。所以,在这种情况下,我不能搞乱domain.equals(..),似乎我需要为仅进行单元测试目的而实现一些比较器,而不是依赖于collection.containsAll(..)。

这里是否有一些可以利用的测试库?您如何测试您的集合?

谢谢。

4个回答

17

我不确定您使用的JUnit版本是什么,但最近的JUnit版本有一个assertThat方法,它将Hamcrest Matcher作为参数。这些匹配器可以组合,因此您可以构建关于集合的复杂断言。

例如,如果您想要断言集合A包含集合B中的每个元素,您可以编写:

import static org.junit.Assert.*;
import static org.junit.matchers.JUnitMatchers.*;
import static org.hamcrest.core.IsCollectionContaining.*;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import org.hamcrest.beans.SamePropertyValuesAs;

public class CollectionTests {

    /*
    * Tests that a contains every element in b (using the equals()
    * method of each element) and that a has the same size as b.
    */
    @Test
    public void test() {
        Collection<Foo> a = doSomething();
        Collection<Foo> b = expectedAnswer;

        assertThat(a, both(hasItems(b)).and(hasSize(b.size())));
    }

    /*
    * Tests that a contains every element in b (using introspection
    * to compare bean properties) and that a has the same size as b.
    */
    @Test
    public void testBeans() {
        Collection<Foo> a = doSomething();
        Collection<Foo> b = expectedAnswer;
        Collection<Matcher<Foo>> bBeanMatchers =
          new LinkedList<Matcher<Foo>>();

        // create a matcher that checks for the property values of each Foo
        for(Foo foo: B)
            bBeanMatchers.add(new SamePropertyValuesAs(foo));

        assertThat(a, both(hasItems(bBeanMatchers)).and(hasSize(b.size())))
    }
}

第一个测试只是在每个对象上使用equalTo()匹配器(它将委托给您的equals实现)。如果这不够强大,您可以使用第二种情况,它将使用getter和setter来比较每个元素。最后,您甚至可以编写自己的匹配器。Hamcrest软件包没有用于按字段匹配(而不是匹配bean属性)的匹配器,但编写FieldMatcher非常简单(事实上是一个很好的练习)。

Matchers一开始有点奇怪,但如果您遵循它们制作新匹配器的示例,有一个返回匹配器的静态方法,您可以进行大量的“import static”,您的代码基本上读起来像英语句子(“断言a既具有b中的项目,又具有与b相同的大小”)。您可以使用这些东西构建出一个相当令人印象深刻的DSL,并使您的测试代码更加优雅。


谢谢提供这些信息。我想我从来没有意识到这些实际上存在。:) 但我不认为它适用于我当前的情况。我刚刚阅读了文档,似乎equalTo()使用Object.equals测试对象相等性,在我的情况下,如果可能的话,我不想混淆我的equals(..)。但是我会记住这个有用的参考资料,以备将来使用。 - limc
对的,equalTo()匹配器使用equals()方法,但SamePropertyValuesAs匹配两个JavaBeans的所有getter和setter。但是,如果您需要匹配私有字段或其他内容,则必须自己编写代码。 - jasonmp85
5
@jasonmp85 这段代码对你仍然有效吗?我在使用 Hamcrest 1.2 时遇到了一个错误:CombinableMatcher 中的 (org.hamcrest.Matcher<? super java.lang.Iterable<java.util.Collection<org.hamcrest.Matcher<org.bitbucket.artbugorski.brainfuj.interpreter.InterpreterTest.Foo>>>>) 无法应用于 (org.hamcrest.Matcher<capture<? super java.util.Collection<? extends java.lang.Object>>>) - Sled
我点赞是因为你向我介绍了SamePropertyValueAs,但是testBeans()方法在junit4.11和hamcrest1.3中已经失效。both..andand上出现问题;hasItems(..)assertThat上也出现问题。 - djeikyb
1
上面的简单test()方法在我这里无法编译(使用Hamcrest 1.3),我得到了error: reference to hasItems is ambiguous, both method <T#1>hasItems(T#1...) in IsCollectionContaining and method <T#2>hasItems(T#2...) in JUnitMatchers match的错误提示? - Matthew Wise

8
如果equals方法没有检查所有字段,您可以使用Unitils的ReflectionAssert类。调用http://unitils.org/中的代码。
ReflectionAssert.assertReflectionEquals(expectedCollection,actualCollection)

将会通过反射逐个比较每个元素的字段(这不仅适用于集合,也适用于任何对象)。


这似乎是我的集合断言的好方法。如果A扩展B,我假设它将考虑来自A和B的字段,这正确吗?我在API文档中找不到它,但我想我可以测试一下。有没有办法指定一个规则仅断言A而不是B的字段? - limc
好的,这很棒...只需阅读文档,似乎我应该使用ReflectionAssert.assertLenientEquals(..)因为它不考虑项目顺序。非常感谢。 - limc
@limc。不用谢。我不确定有没有一种方法可以仅检查子类字段(可能是可能的,我只是从未尝试过)。 - Jeff Storey

1
如果您还没有构建您的集合,另一个选择是:
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.beans.HasPropertyWithValue.hasProperty;
import static org.hamcrest.Matchers.is;

@Test
@SuppressWarnings("unchecked")
public void test_returnsList(){

    arrange();
  
    List<MyBean> myList = act();
    
    assertThat(myList , contains(allOf(hasProperty("id",          is(7L)), 
                                       hasProperty("name",        is("testName1")),
                                       hasProperty("description", is("testDesc1"))),
                                 allOf(hasProperty("id",          is(11L)), 
                                       hasProperty("name",        is("testName2")),
                                       hasProperty("description", is("testDesc2")))));
}

如果您不想检查对象的顺序,请使用containsInAnyOrder

附注:任何帮助避免被抑制的警告都将不胜感激。


0

我无法像jasonmp85的答案那样让最后一部分工作。我包含了我使用的导入,因为某些junit jars为方便起见包含了旧的hamcrest内容。这对我来说有效,但是assert循环明显不如jason的答案中所写的hasItems(..)好用。

import org.hamcrest.Matcher;
import org.hamcrest.beans.SamePropertyValuesAs;
import org.hamcrest.collection.IsCollectionWithSize;

import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.MatcherAssert.assertThat;

...

/*
* Tests that a contains every element in b (using introspection
* to compare bean properties) and that a has the same size as b.
*/
@Test
public void testBeans() {
    Collection<Foo> a = doSomething();
    Collection<Foo> b = expectedAnswer;
    Collection<Matcher<Foo>> bBeanMatchers = new LinkedList<Matcher<Foo>>();

    // create a matcher that checks for the property values of each Foo
    for(Foo foo: B)
        bBeanMatchers.add(new SamePropertyValuesAs(foo));

    // check that each matcher matches something in the list
    for (Matcher<Foo> mf : bBeanMatchers)
        assertThat(a, hasItem(mf));

    // check that list sizes match
    assertThat(a, IsCollectionWithSize.hasSize(b.size()));
}

...

不需要分别使用for循环,这样更简洁:for (final Foo expectedFoo : b) { assertThat(a, hasItem(new SamePropertyValuesAs(expectedFoo))); } - Matthew Wise

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接