4

I have a case class that I serialize to JSON, and a test case that checks that round-tripping works.

Buried deep inside the case class are java.time.Instants, which I put into JSON as their epoch milliseconds.

Turns out that an Instant actually has nanosecond precision and that gets lost in translation, making the test fail because the timestamps are slightly off now.

Is there an easy way to make Scalatest ignore the difference? I just want to fix the test, the loss in precision is totally acceptable for the application.

Thilo
  • 257,207
  • 101
  • 511
  • 656
  • 1
    We have the same problem. That's why we use Clock. And when requesting the current time we use Clock.instant() intstead of Instant.now() and we mock clock.instant at the test to return specific date so we never have precision error – Mahmoud Hanafy Dec 13 '18 at 15:27

2 Answers2

4

We use Clock.instant to know the current time instead of Instant.now to avoid this problem. So the code for the class should be this way

class MyClass(clock: Clock) {
  def getResult(): Result = {
    Result(clock.instant)
  }
}

and at the test, we mock clock.instant to guarantee we check the exact same time.

class MyClassTest {
  val customTime = Instant.now
  val clock = mock[Clock]
  clock.instant() returns customTime

  // test
  val myClass = new MyClass(clock)
  val expectedResult = Result(customTime)
  myClass.getResult ==== expectedResult
}
Mahmoud Hanafy
  • 1,861
  • 3
  • 24
  • 33
  • This is great. TIL about `Clock`. Thanks for that. I am doing a variant of this now: No change to the test code (no mock clock), but inject a production clock with millisecond precision (using `Clock.tickMillis`). But that mocking will sure come in handy in other places. – Thilo Dec 13 '18 at 15:47
  • If you don't want to require injecting a clock, an alternative would be to define a custom `Matcher[T]` which would disregard the nanosecond component (for example, compare the `Instant.toEpochMilli() within the matcher`). – Shane Perry Dec 13 '18 at 16:25
  • 1
    @ShanePerry does that also work when the Instant is buried inside of the object to be matched? – Thilo Dec 13 '18 at 23:13
  • 1
    @Thilo Not directly. Creating a custom matcher will allow you to modify the expected/actual results to zero out the nanosecond component prior to comparison. Assuming a case class, you'll have to copy the instance, replacing the `Instant` value with one where the nanoseconds is zeroed out. `myClass.copy(ts = myclass.ts.with(ChronoField.NANO_OF_SECOND, 0))` where `ts` is an Instant. – Shane Perry Dec 14 '18 at 16:17
4

Truncate to millisecond before comparing

I am not sure it will help in your situation, but in case and for anyone else reading along: the correct way of comparing two Instant objects considering only millisecondes and coarser is to truncate each to millisecond precision (here in Java):

    Instant instant1 = Instant.parse("2018-12-14T08:25:54.232235133Z");
    Instant instant2 = Instant.parse("2018-12-14T08:25:54.232975217Z");
    if (instant1.truncatedTo(ChronoUnit.MILLIS).equals(instant2.truncatedTo(ChronoUnit.MILLIS))) {
        System.out.println("Equal to the millisecond");
    } else {
        System.out.println("Not equal to the millisecond");
    }

Output:

Equal to the millisecond

If you know for a fact that one of them has already been truncated in its roundtrip through JSON (and you consider it a requirement that it must be), you don’t need to truncate that one again, of course.

Use a Clock that has only millisecond presicion

Using a Clock for testing is often a good idea. It can help you write reproducible tests. You can easily have a clock that only counts milliseconds:

    Clock c = Clock.tickMillis(ZoneOffset.UTC);
    System.out.println(c.instant());
    System.out.println(Instant.now(c));

Output when running just now:

2018-12-14T10:48:47.929Z
2018-12-14T10:48:47.945Z

As you see, the generated Instant objects have only three decimals on the seconds, that is, millisecond precision and nothing finer. When you only use the Clock for drawing Instants, it doesn’t matter which time zone you pass to tickMillis.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161