Java List.contains объект с двойным допуском - PullRequest
2 голосов
/ 11 июля 2019

Допустим, у меня есть этот класс:

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 () вычисляет GPA Питера как что-то вроде3.8899999999994, тогда этот тест не пройден, потому что 3.8899999999994! = 3.89.

Я знаю, что могу сделать утверждение с допуском для отдельного значения типа double / float, но есть ли простой способ сделать эту работу с "содержит ", так что мне не нужно сравнивать каждое поле ученика по отдельности (я буду писать много похожих тестов, а сам класс, который я буду тестировать, будет содержать гораздо больше полей).

Мне также нужно избегать изменения рассматриваемого класса (т.е. ученика) для добавления пользовательской логики равенства.

Кроме того, в моем реальном классе будут вложенные списки других двойных значений, которые нуждаются вбыть проверенным с допуском, который еще больше усложнит логику утверждения, если мне придется утверждать каждое поле индивидуально.

В идеале я хотел бы сказать: «Скажите, содержит ли этот список этот студент, и длялюбые поля с плавающей запятой / двойные, сделайте сравнение с допуском .0001 "

Будем благодарны за любые предложения по упрощению этих утверждений.

Ответы [ 4 ]

4 голосов
/ 11 июля 2019

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 с.

4 голосов
/ 11 июля 2019

Поведение 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 голос
/ 11 июля 2019

Я не особенно знаком с концепцией GPA, но я бы предположил, что она никогда не используется за пределами 2 десятичных знаков точности.3.8899999999994 GPA просто не имеет большого смысла, или, по крайней мере, не имеет смысла.

Вы фактически сталкиваетесь с той же проблемой, с которой люди часто сталкиваются при хранении денежных величин.£ 3.89 имеет смысл, а £ 3.88999999 - нет.Там уже есть масса информации для обработки этого.См., Например, эту статью .

TL; DR: Я бы сохранил число как целое число.Таким образом, 3.88 GPA будет храниться как 388. Когда вам нужно вывести значение, просто разделите на 100.0.Целочисленные значения не имеют таких же проблем точности, как значения с плавающей запятой, поэтому ваши объекты, естественно, будет легче сравнивать.

1 голос
/ 11 июля 2019

Если вы хотите использовать contains или equals, тогда вам нужно позаботиться о округлении по методу equals, равному Student.

Однако я рекомендую использовать правильную библиотеку утверждений, такую ​​как AssertJ.

...