JUnit如何测试两个List <E>包含相同顺序的相同元素?


77

语境

我正在为该类编写一个简单的JUnit测试MyObject

MyObject可以从静态工厂方法,需要一个可变参数的创建字符串

MyObject.ofComponents("Uno", "Dos", "Tres");

在存在的任何时间MyObject,客户都可以通过该方法检查以List <E>形式创建的参数.getComponents()

myObject.ofComponents(); // -> List<String>: { "Uno", "Dos", "Tres" }

换句话说,a会MyObject记住并公开使它存在的参数列表。有关此合同的更多详细信息:

  • 的顺序getComponents将与为对象创建选择的顺序相同
  • 允许重复的后续String组件并按顺序保留
  • 行为null未定义(其他代码保证没有null出厂)
  • 对象实例化后,无法更改组件列表

我正在编写一个简单的测试,该测试MyObjectString列表创建一个,并检查它是否可以通过返回相同的列表.getComponents()我立即执行此操作,但这应该在现实的代码路径中一定距离处发生

这是我的尝试:


List<String> argumentComponents = Lists.newArrayList("One", "Two", "Three");
List<String> returnedComponents =
    MyObject.ofComponents(
        argumentComponents.toArray(new String[argumentComponents.size()]))
        .getComponents();
assertTrue(Iterables.elementsEqual(argumentComponents, returnedComponents));

  • 谷歌番石榴 Iterables.elementsEqual()最好的办法,只要我有库在我的构建路径,那些两个列表比较?我一直在为此苦恼;我应该使用遍及Iterable <E> ..检查大小然后迭代运行的.equals()..的辅助方法还是Internet搜索建议的任何其他方法?比较单元测试列表的规范方法是什么?

我很想获得的可选见解

  • 方法测试是否设计合理?我不是JUnit专家!
  • .toArray()List <E>转换为E的可变参数的最佳方法是吗?

13
+1为结构合理的问题。
maba 2012年

1
Hamcrest具有IsIterableContainingInOrder专门用于测试的设计,而不是Iterables。如果发生故障,使用Hamcrest会给出良好的信息。
约翰·B

盛宴的断言提供了一个优雅流畅的API ...
gontard

@JohnB:Hamcrest非常棒,非常适合测试,这就是我要问的(thx!),但是在这种情况下IsIterableContainingInOrder还不够严格,对吗?M={A,B,C}包含N={A,B}M!=N
Robottinosino 2012年

@Robottinosino根据JavadocCreates a matcher for Iterables that matches when a single pass over the examined Iterable yields a series of items, each logically equal to the corresponding item in the specified items. For a positive match, the examined iterable must be of the same length as the number of specified items
John B

Answers:


57

我更喜欢使用Hamcrest,因为如果出现故障,它可以提供更好的输出

Assert.assertThat(listUnderTest, 
       IsIterableContainingInOrder.contains(expectedList.toArray()));

而不是报告

expected true, got false

它会报告

expected List containing "1, 2, 3, ..." got list containing "4, 6, 2, ..."

IsIterableContainingInOrder.contain

Hamcrest

根据Javadoc:

为Iterables创建一个匹配器,当在经过检查的Iterable上单次通过时,该匹配器将产生一系列项目,每个项目在逻辑上均等于指定项目中的相应项目。对于正匹配,所检查的可迭代项的长度必须与指定项目的数量相同

因此,listUnderTest必须具有相同数量的元素,并且每个元素必须按顺序匹配期望值。


1
IsIterableContainingInOrder是否不够严格,无法进行相等性检查?
Robottinosino 2012年

你是什​​么意思/你在问什么?
约翰B

Hamcrest的输出与普通jUnit断言的输出相同。assertEquals(预期的,实际的);将输出:java.lang.AssertionError:预期:<[1,2]>但是:<[3,4]>我认为Hamcrest在输出方式上没有任何好处
henrik

@henrik-Hamcrest的实际输出是Expected: iterable containing [<1>, <3>, <5>, <7>] but: item 3: was <6>。在一长串toString可能输出很长的元素中,了解哪个元素不正确可能会很有帮助。
约翰·B

1
IsIterableContainingInOrder.contain()是误导性的名称。它没有反映这样一个事实,即它额外地验证是否actual具有相同数量的元素(大小相等性检查)。hasSameElementsAs()更合适的名称
Lu55 '19

75

为什么不简单使用List#equals

assertEquals(argumentComponents, imapPathComponents);

合同List#equals

如果两个列表包含相同顺序的相同元素,则两个列表定义为相等。


Hamcrest中哪个匹配者?我可以使用免费提供的精确断言失败消息...
Robottinosino 2012年

1
就像不使用外部库一样
Spektakulatius

恕我直言,此代码看起来比Hamcrest的代码IsIterableContainingInOrder.contain()(其名称未完全反映其行为)清晰得多。因此,如果您具有List <>以外的其他集合,则最好将它们转换为List并使用此代码,而不要使用Hamcrest所需的数组。
Lu55

10

List实现上的equals()方法应该进行元素比较,因此

assertEquals(argumentComponents, returnedComponents);

容易得多。


10

org.junit.Assert.assertEquals()org.junit.Assert.assertArrayEquals()做的工作。

为了避免下一个问题:如果要忽略顺序,请设置所有元素,然后进行比较: Assert.assertEquals(new HashSet<String>(one), new HashSet<String>(two))

但是,如果您只是想忽略重复项,而是保留定单包装,请使用列出LinkedHashSet

另一个提示。Assert.assertEquals(new HashSet<String>(one), new HashSet<String>(two))在比较失败之前,该技巧有效。在这种情况下,它会向您显示错误消息,其中包含集合的字符串表示形式,这可能会造成混淆,因为集合中的顺序几乎是不可预测的(至少对于复杂对象而言)。因此,我发现的技巧是用有序集而不是来包装集合HashSet。您可以TreeSet与自定义比较器一起使用。


7
使用HashSet不会保留顺序和重复项
gontard 2012年

5

为了获得出色的代码可读性,Fest断言断言列表提供了很好的支持

因此,在这种情况下,类似于:

Assertions.assertThat(returnedComponents).containsExactly("One", "Two", "Three");

或者将期望的列表添加到数组中,但是我更喜欢上面的方法,因为它更清晰。

Assertions.assertThat(returnedComponents).containsExactly(argumentComponents.toArray());

3

assertTrue()/ assertFalse():仅用于声明返回的布尔结果

assertTrue(Iterables.elementsEqual(argumentComponents,returnComponents)));

您要使用Assert.assertTrue()Assert.assertFalse()作为被测方法返回一个boolean值。
当方法返回特定的事物(如a)时List,该事物应包含一些预期的元素,因此以assertTrue()这种方式断言:Assert.assertTrue(myActualList.containsAll(myExpectedList) 是反模式。
它使断言易于编写,但是当测试失败时,也使调试变得困难,因为测试运行器只会对您说:

预期true但实际是false

Assert.assertEquals(Object, Object)在JUnit4或Assertions.assertIterableEquals(Iterable, Iterable)JUnit 5中:仅用作两者,equals()并且toString()被比较对象的类(并深入)覆盖

这很重要,因为断言中的相等性测试依赖于equals()并且测试失败消息依赖于toString()比较对象。
由于String覆盖了equals()and toString(),因此断言List<String>with 是完全有效的assertEquals(Object,Object)。关于此问题:您必须equals()在类中进行重写,因为在对象相等性方面它是有意义的,不仅要使使用JUnit进行测试时的断言更容易。
为了使声明更容易,您可以采用其他方法(可以在答案的接下来的部分中看到)。

Guava是执行/构建单元测试断言的一种方法吗?

Google Guava Iterables.elementsEqual()是最好的方法,前提是我在构建路径中有库来比较这两个列表?

不它不是。Guava不是一个编写单元测试断言的库。
您不需要它来编写大多数(我认为)的单元测试。

比较单元测试列表的规范方法是什么?

作为一个好习惯,我赞成断言/匹配器库。

我不能鼓励JUnit执行特定的断言,因为它提供的功能实在太少且功能有限:它仅执行具有深等式的断言。
有时您希望允许元素中的任何顺序,有时您希望允许期望中的任何元素与实际元素匹配,因此...

因此,使用单元测试断言/匹配器库(例如Hamcrest或AssertJ)是正确的方法。
实际答案提供了Hamcrest解决方案。这是一个AssertJ解决方案。

org.assertj.core.api.ListAssert.containsExactly() 是您所需要的:它按顺序验证实际组中是否完全包含给定的值,而没有其他内容:

验证实际组是否按顺序完全包含给定的值,并且没有其他内容。

您的测试可能如下所示:

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

@Test
void ofComponent_AssertJ() throws Exception {
   MyObject myObject = MyObject.ofComponents("One", "Two", "Three");
   Assertions.assertThat(myObject.getComponents())
             .containsExactly("One", "Two", "Three");
}

AssertJ的一个优点List是不需要按预期声明a :它使断言更直接,代码更易读:

Assertions.assertThat(myObject.getComponents())
         .containsExactly("One", "Two", "Three");

如果测试失败:

// Fail : Three was not expected 
Assertions.assertThat(myObject.getComponents())
          .containsExactly("One", "Two");

您会收到一条非常清晰的消息,例如:

java.lang.AssertionError:

期望:

<[“一个”,“两个”,“三个”]>

完全包含(并且顺序相同):

<[“一个”,“两个”]>

但是某些元素是无法预期的:

<[“三个”]>

断言/匹配器库是必须的,因为它们确实会进一步发展

假设其中MyObject不存储,String而是存储Foo的实例,例如:

public class MyFooObject {

    private List<Foo> values;
    @SafeVarargs
    public static MyFooObject ofComponents(Foo... values) {
        // ...
    }

    public List<Foo> getComponents(){
        return new ArrayList<>(values);
    }
}

这是非常普遍的需求。使用AssertJ,断言仍然很容易编写。更好的是,您可以断言列表内容是相等的,即使equals()/hashCode()在JUnit方法要求时元素的类没有被覆盖的情况下:

import org.assertj.core.api.Assertions;
import static org.assertj.core.groups.Tuple.tuple;
import org.junit.jupiter.api.Test;

@Test
void ofComponent() throws Exception {
    MyFooObject myObject = MyFooObject.ofComponents(new Foo(1, "One"), new Foo(2, "Two"), new Foo(3, "Three"));

    Assertions.assertThat(myObject.getComponents())
              .extracting(Foo::getId, Foo::getName)
              .containsExactly(tuple(1, "One"),
                               tuple(2, "Two"),
                               tuple(3, "Three"));
}

1
  • 我对是否Iterables.elementsEqual是最佳选择的回答:

Iterables.elementsEqual足以比较2 Lists。

Iterables.elementsEqual用于更通用的场景,它接受更通用的类型:Iterable。也就是说,您甚至可以将aListSet。(按迭代顺序,这很重要)

确定ArrayListLinkedList定义equals非常好,您可以直接调用equals。当您使用定义不明确的列表时,Iterables.elementsEqual是最佳选择。应该注意一件事:Iterables.elementsEqual不接受null

  • 要将List转换为array:Iterables.toArray比较容易。

  • 对于单元测试,建议将空列表添加到测试用例。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.