9

Rails supports DateTime with resolution of nanoseconds, but I noticed that it depends on the machine the app is being run.

On my MacBook running Mojave, Time.zone.now.strftime("%N") will always output a 9-digit number that ends in 000, (e.g. "122981000"). It means that on the Mac, the resolution is limited to the microseconds scale.

On a Linux, however, the same command returns a number with full nanoseconds resolution (e.g. "113578523").

My problem comes when I'm using rspec and I need to compare some DateTime values.
When I'm developing on my Mac, the tests pass flawlessly, but then when our CI (Travis) will run the same test, it fails like this:

       expected: 2019-04-09 19:14:27.214939637 -0300
            got: 2019-04-09 19:14:27.214939000 -0300

The issue here is that our database, Postgres, is limited to microseconds, just like my Mac, where it doesn't fail. I store the DateTime in the DB, then read it back and compare to what I have in memory. The DB rounds to microseconds, and therefore the comparison fails.

Is it possible to force Rails to run using microseconds precision?
My intention is to not need to manually round or truncate the timestamps in every test.

Guilherme
  • 7,839
  • 9
  • 56
  • 99
  • 1
    You can use `be_within` matcher and check the difference of 0.1 second. please refer https://stackoverflow.com/a/26207378/5306420 – punitcse Apr 11 '19 at 12:40
  • Also If you don't care about millisecond difference, you could do a to_s/to_i on both sides of your expectation https://stackoverflow.com/a/20403290/5306420 – punitcse Apr 11 '19 at 12:42
  • `be_within` is still a solution that needs to be applied to every test that compare timestamps. I would like to find a solution that requires a single change, if possible – Guilherme Apr 11 '19 at 12:43

2 Answers2

2

I've run into this before in the past, where newly created ActiveRecord objects would have 9 digits of precision on the created / updated fields in memory (on Travis linux VM) and this would be different to what was stored in Postgres (6 digits). I had the same issue that these tests passed locally (macOS) but failed in CI build.

In the past I had hacked specs to force an object reload which worked but as you highlight is ugly because you have to do it for every test. Recently I came across the same issue again but this time with a service object I had created that could not be 'reloaded' so I set about trying to find a better solution.

It's not exactly ideal as it require monkey patching the Time class but, it works!

module ForceTimePrecision
  def now
    super.round(6)
  end
end

Time.singleton_class.send(:prepend, ForceTimePrecision)

the mechanism to patch the Time singleton (as I couldn't just override Time.now directly) is based on this answer https://stackoverflow.com/a/60665577/989981

sgbett
  • 348
  • 3
  • 15
0

Rounding as sgbett answer suggest would sometimes round up or down so it would not match in my case, on my app we don't need more precision than seconds so we added a matcher to be used in this cases.

RSpec::Matchers.define :eql_time do |expected|
    match do |actual|
      # Rails has more precision than postgres so datetime objects do not match unless
      # matched up to seconds
      expected.to_i == actual.to_i
    end
end
Guillermo Siliceo Trueba
  • 4,251
  • 5
  • 34
  • 47