2

Let's say I have this class:

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

  // Assume constructor here...
}

And I have a test something like:

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

Now, if the getStudents() method calculates Peter's GPA as something like 3.8899999999994, then this test will fail because 3.8899999999994 != 3.89.

I know that I can do an assertion with a tolerance for an individual double/float value, but is there an easy way to make this work with "contains", so that I don't have to compare each field of Student individually (I will be writing many similar tests, and the actual class I will be testing will contain many more fields).

I also need to avoid modifying the class in question (i.e. Student) to add custom equality logic.

Also, in my actual class, there will be nested lists of other double values that need to be tested with a tolerance, which will complicate the assertion logic even more if I have to assert each field individually.

Ideally, I'd like to say "Tell me if this list contains this student, and for any float/double fields, do the comparison with tolerance of .0001"

Any suggestions to keep these assertions simple are appreciated.

JoeMjr2
  • 3,804
  • 4
  • 34
  • 62
  • 1
    There is no way to use the built in `contains` method (which just uses `.equals()` under the hood) with those constraints. So you'd have to override `equals`. – ajax992 Jul 11 '19 at 18:07
  • 1
    Do you have an actual failing case, or is this question based on worries about things you have heard about floating-point arithmetic? “Comparing with a tolerance” is not a good solution. A good solution could be ensure that a GPA is always correctly computed to two decimal places (by writing good code for that) and storing it with two decimal places (as with an integer containing 100 times the represented value). – Eric Postpischil Jul 11 '19 at 18:11

4 Answers4

7

1) Don't override equals/hashCode only for unit testing purposes

These methods have a semantic and their semantic is not taking into consideration all fields of the class to make a test assertion possible.

2) Rely on testing library to perform your assertions

Assert(students.contains(expectedStudent)

or that (posted in the John Bollinger answer):

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

are great anti patterns in terms of unit testing.
When an assertion fails, the first thing that you need is knowing the cause of the error to correct the test.
Relying on a boolean to assert the list comparison doesn't allow that at all.
KISS (Keep it simple and stupid): Use testing tools/features to assert and don't reinvent the wheel because these will provide the feedback needed when your test fails.

3) Don't assert double with equals(expected, actual).

To assert double values, unit testing libraries provide a third parameter in the assertion to specify the allowed delta such as :

public static void assertEquals(double expected, double actual, double delta) 

in JUnit 5 (JUnit 4 has a similarly thing).

Or favor BigDecimal to double/float that is more suitable for this kind of comparison.

But it will not completely solve your requirement as you need to assert multiple fields of your actual object. Using a loop to do that is clearly not a fine solution.
Matcher libraries provide a meaningful and elegant way to solve that.

4) Use Matcher libraries to perform assertions on specific properties of objects of the actual List

With 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(...),
          );

Some explanations (all of these are AssertJ features) :

  • usingComparatorForType() allows to set a specific comparator for the given type of elements or their fields.

  • DoubleComparator is a AssertJ comparator providing the facility to take an epsilon into consideration in the double comparison.

  • extracting defines values to assert from the instances contained in the List.

  • containsExactly() asserts that the extracted values are exactly (that is no more, no less and in the exact order) these defined in the Tuples.

davidxxx
  • 125,838
  • 23
  • 214
  • 215
4

The behavior of List.contains() is defined in terms of the equals() methods of the elements. Therefore, if your Student.equals() method compares gpas for exact equality and you cannot change it then List.contains() is not a viable method for your purpose.

And probably Student.equals() shouldn't use a comparison with tolerance, because it's very hard to see how you could make that class's hashCode() method consistent with such an equals() method.

Perhaps what you can do is write an alternative, equals-like method, say "matches()", that contains your fuzzy-comparison logic. You could then test a list for a student fitting your criteria with something like

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

There is an implicit iteration in that, but the same is true of List.contains().

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • Yes, @Derefacto, that might help if it's viable (all the `Student` properties exposed via setters). In that case, one could also go all the way to a custom `Predicate` implementation to use in place of a lambda. But note that the OP didn't say he couldn't modify `Student` *at all*. Rather, he said he couldn't modify it *to add custom equality logic*. If that just means "I can't change the `equals()` method" then implementing `matches()` on `Student` is a pretty reasonable thing to do. – John Bollinger Jul 11 '19 at 18:31
  • Using `anyMatch()` is really not the way to assert in an unit test. When the test fails you want to get a feeback on the failure. – davidxxx Jul 11 '19 at 18:43
  • Perhaps you're right, @davidxxx, but `anyMatch()` is a pretty close analog of what the OP proposed to do by using `contains()`, which is the subject of the actual question. – John Bollinger Jul 11 '19 at 18:50
  • 1
    The OP wants to unit-test something, so testing is IHMO an important thing here. Question is not how to write this code in Java 8. Much people use stackoverflow as reference. I am really annoying as a bad practice is too much upvoted because people that read this post may follow it. Which will produce technical debt. And that is really not great. – davidxxx Jul 11 '19 at 18:54
1

If you want to use contains or equals, then you need to take care of rounding in the equals method of Student.

However, I recommend using a proper assertion library such as AssertJ.

Michael
  • 1,044
  • 5
  • 9
1

I'm not particularly familiar with the concept of GPA, but I would imagine that it it's never used beyond 2 decimal places of precision. A 3.8899999999994 GPA simply doesn't make a great deal of sense, or at least is not meaningful.

You are effectively facing the same problem that people often face when storing monetary values. £3.89 makes sense, but £3.88999999 does not. There is a wealth of information out there already for handling this. See this article, for example.

TL;DR: I would store the number as an integer. So 3.88 GPA would be stored as 388. When you need to print the value, simply divide by 100.0. Integers do not have the same precision problems as floating point values, so your objects will naturally be easier to compare.

Michael
  • 41,989
  • 11
  • 82
  • 128
  • GPA was only an example of a calculated value. My real use case will need a more arbitrary precision. – JoeMjr2 Jul 15 '19 at 16:09