让我说我上了这个课:
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的比较”
任何使这些断言保持简单的建议都会受到赞赏。
答案 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)
如果要使用contains
或equals
,则需要注意equals
的{{1}}方法的舍入。
但是,我建议使用适当的断言库,例如AssertJ。
答案 3 :(得分:1)
我对GPA的概念不是特别熟悉,但是我可以想象,它从未超过2个小数位的精度。 3.8899999999994 GPA根本没有多大意义,或者至少没有意义。
您实际上正面临着人们在存储货币价值时经常面临的相同问题。 £3.89才有意义,但£3.88999999则没有意义。已经有大量信息可用于处理此问题。例如,请参见this article。
TL; DR:我将数字存储为整数。因此,3.88 GPA将存储为388。当您需要打印值时,只需除以100.0
。整数不具有与浮点值相同的精度问题,因此您的对象自然很容易比较。