Java List.contains объект с двойным допуском
Допустим, у меня есть этот класс:
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() вычисляет средний балл Питера как 3.8899999999994, тогда этот тест не пройден, потому что 3.8899999999994!= 3.89.
Я знаю, что могу сделать утверждение с допустимым отклонением для отдельного значения типа double/float, но есть ли простой способ выполнить эту работу с помощью "содержимого", чтобы мне не приходилось сравнивать каждое поле Student отдельно (я я буду писать много похожих тестов, а сам класс, который я буду тестировать, будет содержать гораздо больше полей).
Мне также нужно избегать изменения рассматриваемого класса (например, ученика) для добавления пользовательской логики равенства.
Кроме того, в моем реальном классе будут вложенные списки других двойных значений, которые необходимо проверить с допуском, что еще больше усложнит логику утверждения, если мне придется утверждать каждое поле индивидуально.
В идеале я хотел бы сказать: "Скажите, содержит ли этот список этот ученик, и для любых полей с плавающей запятой / двойного числа сделайте сравнение с допуском 0,0001".
Любые предложения, чтобы сохранить эти утверждения простыми, приветствуются.
4 ответа
Поведение List.contains()
определяется с точки зрения equals()
методы элементов. Поэтому, если ваш Student.equals()
метод сравнивает gpas для точного равенства, и вы не можете изменить его List.contains()
не является жизнеспособным методом для вашей цели.
И наверное Student.equals()
не следует использовать сравнение с допуском, потому что очень трудно понять, как вы могли бы сделать этот класс hashCode()
Метод согласуется с таким equals()
метод.
Возможно, что вы можете сделать, это написать альтернативу, equals
-подобный метод, скажемmatches()
", который содержит вашу логику нечеткого сравнения. Затем вы можете проверить список для ученика, который соответствует вашим критериям с чем-то вроде
Assert(students.stream().anyMatch(s -> expectedStudent.matches(s)));
В этом есть неявная итерация, но то же самое верно и для List.contains()
,
1) Не переопределяйте equals/hashCode только для целей модульного тестирования
Эти методы имеют семантику, и их семантика не учитывает все поля класса, чтобы сделать возможным тестовое утверждение.
2) Положитесь на тестирование библиотеки для выполнения ваших утверждений
Assert(students.contains(expectedStudent)
или что (опубликовано в ответе Джона Боллинджера):
Assert(students.stream().anyMatch(s -> expectedStudent.matches(s)));
большие анти-паттерны с точки зрения юнит-тестирования.
Когда утверждение не выполняется, первое, что вам нужно, это знать причину ошибки, чтобы исправить тест.
Полагаясь на логическое значение, чтобы утверждать, что сравнение списка не позволяет этого вообще.
ПОЦЕЛУЙ (Пусть это будет просто и глупо): используйте инструменты / функции тестирования, чтобы утверждать, и не изобретайте колесо заново, потому что они обеспечат обратную связь, необходимую, когда ваш тест не пройден.
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 для выполнения утверждений о конкретных свойствах объектов актуального Списка
С 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
определяет значения для утверждения из экземпляров, содержащихся в списке.containsExactly()
утверждает, что извлеченные значения в точности (то есть не больше, не меньше и в точном порядке) они определены вTuple
s.
Если вы хотите использовать contains
или же equals
, то вам нужно позаботиться о округлении в equals
метод Student
,
Тем не менее, я рекомендую использовать правильную библиотеку утверждений, такую как AssertJ.
Я не особенно знаком с концепцией GPA, но я бы предположил, что она никогда не используется с точностью до двух десятичных знаков точности. 3.8899999999994 GPA просто не имеет большого смысла, или, по крайней мере, не имеет смысла.
Вы фактически сталкиваетесь с той же проблемой, с которой люди часто сталкиваются при хранении денежных величин. £3.89 имеет смысл, а £3.88999999 - нет. Там уже есть масса информации для обработки этого. Смотрите эту статью, например.
TL; DR: я бы сохранил число как целое число. Таким образом, 3.88 ГПД будет храниться как 388. Когда вам нужно вывести значение, просто разделите на 100.0
, Целые числа не имеют таких же проблем точности, как значения с плавающей запятой, поэтому ваши объекты, естественно, будет легче сравнивать.