Time values changing during examples

2 views
Skip to first unread message

Jack Royal-Gordon

unread,
Feb 25, 2021, 1:58:38 PMFeb 25
to rs...@googlegroups.com
I have the following examples:

  describe "#subscription_cancelled_at" do
    let!(:user) { FactoryBot.create(:user, stripe_id: 'stripe') }
    let!(:stripe_customer) { Stripe::Customer.new }
    let!(:past_date) { 2.weeks.ago }
    let!(:future_date) { 2.weeks.from_now }
    let!(:subscription) { build_deep_struct(status: 'active', cancel_at_period_end: false, canceled_at: nil, current_period_end: future_date) }

    it 'returns nil if there is no subscription' do
      allow(user).to receive(:stripe_customer).and_return(nil)
      expect(user).to receive(:stripe_customer)
      expect(stripe_customer).to_not receive(:subscriptions)
      expect(user.subscription_cancelled_at).to be_nil
    end
    
    it "returns canceled_at if it is populated" do
      subscription.canceled_at = past_date
      allow(user).to receive(:stripe_customer).and_return(stripe_customer)
      allow(stripe_customer).to receive(:subscriptions).and_return([subscription])
      expect(user).to receive(:stripe_customer)
      expect(stripe_customer).to receive(:subscriptions)
      expect(user.subscription_cancelled_at.utc).to eq past_date
    end
    
    it "returns current_period_end if subscription will cancel at period end" do
      subscription.cancel_at_period_end = true
      allow(user).to receive(:stripe_customer).and_return(stripe_customer)
      allow(stripe_customer).to receive(:subscriptions).and_return([subscription])
      expect(user).to receive(:stripe_customer)
      expect(stripe_customer).to receive(:subscriptions)
      expect(user.subscription_cancelled_at).to eq future_date
    end
    
    it 'returns false otherwise' do
      allow(user).to receive(:stripe_customer).and_return(stripe_customer)
      allow(stripe_customer).to receive(:subscriptions).and_return([subscription])
      expect(user).to receive(:stripe_customer)
      expect(stripe_customer).to receive(:subscriptions)
      expect(user.subscription_cancelled_at).to be_nil
    end
  end

All that’s really going on here is that I’m passing the dates through some data structures and eventually returning the expected dates. There is no date math (or conversion) involved behind the scenes. However, I get these results:

  1) User::StripeCustomer#subscription_cancelled_at returns canceled_at if it is populated
     Failure/Error: expect(user.subscription_cancelled_at.utc).to eq @@past_date

     

       expected: 2021-02-11 18:48:06.455547000 +0000
            got: 2021-02-11 18:48:06.455546855 +0000

     

       (compared using ==)

     

       Diff:
       @@ -1 +1 @@
       -Thu, 11 Feb 2021 18:48:06 UTC +00:00
       +2021-02-11 18:48:06 UTC

       

     # ./spec/models/user/stripe_customer_spec.rb:312:in `block (3 levels) in <top (required)>'

  2) User::StripeCustomer#subscription_cancelled_at returns current_period_end if subscription will cancel at period end
     Failure/Error: expect(user.subscription_cancelled_at).to eq @@future_date

     

       expected: 2021-03-11 18:48:06.455753000 +0000
            got: 2021-03-11 10:48:06.455752849 -0800

     

       (compared using ==)

     

       Diff:
       @@ -1 +1 @@
       -Thu, 11 Mar 2021 18:48:06 UTC +00:00
       +2021-03-11 10:48:06 -0800

       

     # ./spec/models/user/stripe_customer_spec.rb:321:in `block (3 levels) in <top (required)>'


Notice that the times being compared are off by very small amounts (< 1 sec). I tried replacing the “let!” commands with assignments to class variables (@@past_date, etc) in an attempt to ensure that the let! blocks were not being re-evaluated each time they were referenced, but that was not the case.

I can certainly change the test to check for a time “close” to the expected time, but that is a hack. Can anyone explain why I’m getting these differences?

Joseph Haig

unread,
Feb 25, 2021, 3:22:13 PMFeb 25
to rs...@googlegroups.com
I don't know exactly what is going on in your `subscription_cancelled_at` method but generally if you are trying to run tests with time involved you want to use something like Timecop (https://github.com/travisjeffery/timecop) or, if you are using Rails, the Rails time helpers (https://api.rubyonrails.org/v6.1.3/classes/ActiveSupport/Testing/TimeHelpers.html). This will allow you to change the time at will and to freeze time so that you do not see small differences that are most likely due to the time it takes to run your code.

Regards,

Joe

--
You received this message because you are subscribed to the Google Groups "rspec" group.
To unsubscribe from this group and stop receiving emails from it, send an email to rspec+un...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/rspec/E9584261-2BFC-49BB-AC53-21991545F39F%40pobox.com.

Jack Royal-Gordon

unread,
Feb 25, 2021, 3:38:40 PMFeb 25
to rs...@googlegroups.com
“subscription_cancelled_at merely retrieves an object (which I’m mocking in the “build_deep_struct” call, which is just a beefed up Struct class generator). No calcs are being done, no calls to get time (other than the two “let!” statements), no time math. It’s just building an object that is a struct (in the case of #build_deep_struct) and pulling out properties of the object (in the case of #subscription_canceled_at). I do use TimeCop when I’m doing match or testing code that retrieves date/time values, but this is not one of those cases.  We’re literally doing the following:

let (:time_value) { 2.weeks.ago }


some_strucuture.some_property = time_value


expect(some_strucuture.some_property).to eq time_value


Of course, there is some code in between to make it less obvious, but that’s fundamentally what’s going on.

Phil Pirozhkov

unread,
Feb 25, 2021, 3:44:33 PMFeb 25
to Jack Royal-Gordon
Jack,

I'd like a sneak peak into `subscription_cancelled_at`'s code.
Some lossy conversions may happen behind the scenes. See
https://github.com/rails/rails/issues/38831#issuecomment-606146664

I'd definitely not rely on micro- and milli-second precision in expectations.
To my best memory Rails opted to round to seconds in their
ActiveJob-related test helpers.

With RSpec, I'd suggest using be_within(1.second).of(future_date)

- Phil

Jack Royal-Gordon

unread,
Feb 25, 2021, 4:55:16 PMFeb 25
to rs...@googlegroups.com
Hi Phil,

I feel like such an idiot. I was well aware of the lossy-conversion issue, but had eliminated it as a possibility because I was “sure” that there was no conversion going on in the backend. When I went to grab the code, there it was: “Time.at()”.  Sigh …

Thanks for making me re-examine my assumptions.

Please take a look at my other issue — there’s no “behind the scenes” code there.

Jack


Reply all
Reply to author
Forward
0 new messages