Java List.contains对象具有双精度公差

时间:2019-07-11 17:58:48

标签: java unit-testing collections floating-point floating-point-comparison

让我说我上了这个课:

public class Student
{
  long studentId;
  String name;
  double gpa;

  // Assume constructor here...
}

我进行了类似的测试:

List<Student> students = getStudents();
Student expectedStudent = new Student(1234, "Peter Smith", 3.89)
Assert(students.contains(expectedStudent)

现在,如果getStudents()方法将Peter的GPA计算为3.8899999999994,则该测试将失败,因为3.8899999999994!= 3.89。

我知道我可以对单个double / float值进行容忍声明,但是有一种简单的方法可以使“ contains”起作用,因此我不必比较Student的每个字段单独(我将编写许多类似的测试,而我将要测试的实际类将包含更多字段)。

我还需要避免修改相关的类(即学生)以添加自定义相等逻辑。

此外,在我的实际班级中,将存在其他双精度值的嵌套列表,需要对其进行容限测试,如果我必须分别声明每个字段,则声明逻辑将变得更加复杂。

理想情况下,我想说:“如果此列表包含此学生,请告诉我,对于任何浮点/双精度字段,请进行公差为.0001的比较”

任何使这些断言保持简单的建议都会受到赞赏。

4 个答案:

答案 0 :(得分:4)

List.contains()的行为是根据元素的equals()方法定义的。因此,如果您的Student.equals()方法比较gpa精确相等,而您无法更改它,那么List.contains()就您而言不是可行的方法。

也许Student.equals() 不应该使用容差进行比较,因为很难看到如何使该类的hashCode()方法与这样的{{ 1}}方法。

也许您可以做的是编写另一种类似equals()的方法,说“ equals”,其中包含您的模糊比较逻辑。然后,您可以为符合条件的学生测试列表,例如

matches()

其中存在一个隐式迭代,但是Assert(students.stream().anyMatch(s -> expectedStudent.matches(s))); 的情况也是如此。

答案 1 :(得分:4)

1)不要仅出于单元测试目的而覆盖equals / hashCode

这些方法具有语义,并且它们的语义并未考虑类的所有字段以使测试断言成为可能。

2)依靠测试库执行您的断言

Assert(students.contains(expectedStudent)

或那个(张贴在约翰·布林格答案中):

Assert(students.stream().anyMatch(s -> expectedStudent.matches(s)));

在单元测试方面是很好的反模式。
断言失败时,您需要做的第一件事就是了解错误原因以更正测试。
依靠布尔来断言列表比较根本不允许这样做。
KISS(保持简单而又愚蠢):使用测试工具/功能来断言并且不要重新发明轮子,因为当测试失败时,这些工具/功能将提供所需的反馈。

3)不要对double声明equals(expected, actual)

要声明双精度值,单元测试库在声明中提供第三个参数以指定允许的增量,例如:

public static void assertEquals(double expected, double actual, double delta) 
JUnit 5中的

(JUnit 4也有类似的东西)。

或者将BigDecimal推荐给double/float,使其更适合这种比较。

但是由于您需要声明实际对象的多个字段,因此它不能完全解决您的要求。使用循环执行此操作显然不是一个好方法。
Matcher库提供了一种有意义且优雅的方法来解决该问题。

4)使用Matcher库对实际List对象的特定属性执行断言

使用AssertJ:

//GIVEN
...

//WHEN
List<Student> students = getStudents();

//THEN
Assertions.assertThat(students)
           // 0.1 allowed delta for the double value
          .usingComparatorForType(new DoubleComparator(0.1), Double.class) 
          .extracting(Student::getId, Student::getName, Student::getGpa)
          .containsExactly(tuple(1234, "Peter Smith", 3.89),
                           tuple(...),
          );

一些说明(所有这些都是AssertJ功能):

  • usingComparatorForType()允许为给定类型的元素或其字段设置特定的比较器。

  • DoubleComparator是AssertJ比较器,提供了在双重比较中考虑ε的功能。

  • extracting定义要从List中包含的实例中声明的值。

  • containsExactly()断言所提取的值与Tuple中定义的值完全相同(即不多,且不多,且按准确顺序)。

答案 2 :(得分:1)

如果要使用containsequals,则需要注意equals的{​​{1}}方法的舍入。

但是,我建议使用适当的断言库,例如AssertJ。

答案 3 :(得分:1)

我对GPA的概念不是特别熟悉,但是我可以想象,它从未超过2个小数位的精度。 3.8899999999994 GPA根本没有多大意义,或者至少没有意义。

您实际上正面临着人们在存储货币价值时经常面临的相同问题。 £3.89才有意义,但£3.88999999则没有意义。已经有大量信息可用于处理此问题。例如,请参见this article

TL; DR:我将数字存储为整数。因此,3.88 GPA将存储为388。当您需要打印值时,只需除以100.0。整数不具有与浮点值相同的精度问题,因此您的对象自然很容易比较。