Trouble comparing time with RSpec - ruby-on-rails
I am using Ruby on Rails 4 and the rspec-rails gem 2.14. For a my object I would like to compare the current time with the updated_at object attribute after a controller action run, but I am in trouble since the spec does not pass. That is, given the following is the spec code:
it "updates updated_at attribute" do
Timecop.freeze
patch :update
#article.reload
expect(#article.updated_at).to eq(Time.now)
end
When I run the above spec I get the following error:
Failure/Error: expect(#article.updated_at).to eq(Time.now)
expected: 2013-12-05 14:42:20 UTC
got: Thu, 05 Dec 2013 08:42:20 CST -06:00
(compared using ==)
How can I make the spec to pass?
Note: I tried also the following (note the utc addition):
it "updates updated_at attribute" do
Timecop.freeze
patch :update
#article.reload
expect(#article.updated_at.utc).to eq(Time.now)
end
but the spec still does not pass (note the "got" value difference):
Failure/Error: expect(#article.updated_at.utc).to eq(Time.now)
expected: 2013-12-05 14:42:20 UTC
got: 2013-12-05 14:42:20 UTC
(compared using ==)
I find using the be_within default rspec matcher more elegant:
expect(#article.updated_at.utc).to be_within(1.second).of Time.now
Ruby Time object maintains greater precision than the database does. When the value is read back from the database, it’s only preserved to microsecond precision, while the in-memory representation is precise to nanoseconds.
If you don't care about millisecond difference, you could do a to_s/to_i on both sides of your expectation
expect(#article.updated_at.utc.to_s).to eq(Time.now.to_s)
or
expect(#article.updated_at.utc.to_i).to eq(Time.now.to_i)
Refer to this for more information about why the times are different
Old post, but I hope it helps anyone who enters here for a solution. I think it's easier and more reliable to just create the date manually:
it "updates updated_at attribute" do
freezed_time = Time.utc(2015, 1, 1, 12, 0, 0) #Put here any time you want
Timecop.freeze(freezed_time) do
patch :update
#article.reload
expect(#article.updated_at).to eq(freezed_time)
end
end
This ensures the stored date is the right one, without doing to_x or worrying about decimals.
yep as Oin is suggesting be_within matcher is the best practice
...and it has some more uscases -> http://www.eq8.eu/blogs/27-rspec-be_within-matcher
But one more way how to deal with this is to use Rails built in midday and middnight attributes.
it do
# ...
stubtime = Time.now.midday
expect(Time).to receive(:now).and_return(stubtime)
patch :update
expect(#article.reload.updated_at).to eq(stubtime)
# ...
end
Now this is just for demonstration !
I wouldn't use this in a controller as you are stubbing all Time.new calls => all time attributes will have same time => may not prove concept you are trying to achive. I usually use it in composed Ruby Objects similar to this:
class MyService
attr_reader :time_evaluator, resource
def initialize(resource:, time_evaluator: ->{Time.now})
#time_evaluator = time_evaluator
#resource = resource
end
def call
# do some complex logic
resource.published_at = time_evaluator.call
end
end
require 'rspec'
require 'active_support/time'
require 'ostruct'
RSpec.describe MyService do
let(:service) { described_class.new(resource: resource, time_evaluator: -> { Time.now.midday } ) }
let(:resource) { OpenStruct.new }
it do
service.call
expect(resource.published_at).to eq(Time.now.midday)
end
end
But honestly I recommend to stick with be_within matcher even when comparing Time.now.midday !
So yes pls stick with be_within matcher ;)
update 2017-02
Question in comment:
what if the times are in a Hash? any way to make expect(hash_1).to eq(hash_2) work when some hash_1 values are pre-db-times and the corresponding values in hash_2 are post-db-times? –
expect({mytime: Time.now}).to match({mytime: be_within(3.seconds).of(Time.now)}) `
you can pass any RSpec matcher to the match matcher
(so e.g. you can even do API testing with pure RSpec)
As for "post-db-times" I guess you mean string that is generated after saving to DB. I would suggest decouple this case to 2 expectations (one ensuring hash structure, second checking the time) So you can do something like:
hash = {mytime: Time.now.to_s(:db)}
expect(hash).to match({mytime: be_kind_of(String))
expect(Time.parse(hash.fetch(:mytime))).to be_within(3.seconds).of(Time.now)
But if this case is too often in your test suite I would suggest writing your own RSpec matcher (e.g. be_near_time_now_db_string) converting db string time to Time object and then use this as a part of the match(hash) :
expect(hash).to match({mytime: be_near_time_now_db_string}) # you need to write your own matcher for this to work.
The easiest way I found around this problem is to create a current_time test helper method like so:
module SpecHelpers
# Database time rounds to the nearest millisecond, so for comparison its
# easiest to use this method instead
def current_time
Time.zone.now.change(usec: 0)
end
end
RSpec.configure do |config|
config.include SpecHelpers
end
Now the time is always rounded to the nearest millisecond to comparisons are straightforward:
it "updates updated_at attribute" do
Timecop.freeze(current_time)
patch :update
#article.reload
expect(#article.updated_at).to eq(current_time)
end
You can convert the date/datetime/time object to a string as it's stored in the database with to_s(:db).
expect(#article.updated_at.to_s(:db)).to eq '2015-01-01 00:00:00'
expect(#article.updated_at.to_s(:db)).to eq Time.current.to_s(:db)
Because I was comparing hashes, most of these solutions did not work for me so I found the easiest solution was to simply grab the data from the hash I was comparing. Since the updated_at times are not actually useful for me to test this works fine.
data = { updated_at: Date.new(2019, 1, 1,), some_other_keys: ...}
expect(data).to eq(
{updated_at: data[:updated_at], some_other_keys: ...}
)
In Rails 4.1+ you can use Time Helpers:
include ActiveSupport::Testing::TimeHelpers
describe "some test" do
around { |example| freeze_time { example.run } }
it "updates updated_at attribute" do
expect { patch :update }.to change { #article.reload.updated_at }.to(Time.current)
end
end
Related
Overriding an active support method with a ruby refinement
I'd like to use a Ruby Refinement to monkey patch an ActiveSupport method for outputting time in a specific format. My ultimate goal is to have JSON.pretty_generate(active_record.as_json) print all timestamps in UTC, iso8601, 6 decimals. And I want to have all other timestamp printing behave normally. This is what I have so far: module ActiveSupportExtensions refine ActiveSupport::TimeWithZone do def to_s(_format = :default) utc.iso8601(6) end end end class Export using ActiveSupportExtensions def export puts JSON.pretty_generate(User.last.as_json(only: [:created_at])) end end Export.new.export Which outputs the following (not what I want). { "created_at": "2022-04-05 14:36:07 -0700" } What's interesting, is if I monkey patch this the regular way: class ActiveSupport::TimeWithZone def to_s utc.iso8601(6) end end puts JSON.pretty_generate(User.last.as_json(only: [:created_at])) I get exactly what I want: { "created_at": "2022-04-05T21:36:07.878101Z" } The only issue is that this overrides the entire applications TimeWithZone class, which is not something I want to do for obvious reasons.
Thanks to Lam Phan comment, it's not possible via a refinement unfortunately. However I was able to do it by override the default timestamp format. # save previous default value previous_default = ::Time::DATE_FORMATS[:default] # set new default to be the utc timezone with iso8601 format ::Time::DATE_FORMATS[:default] = proc { |time| time.utc.iso8601(6) } puts JSON.pretty_generate(User.last.as_json(only: [:created_at])) # set the default back if we have one if previous_default.blank? ::Time::DATE_FORMATS.delete(:default) else ::Time::DATE_FORMATS[:default] = previous_default end
RSpec equality matcher failing for serializer test
I am writing a test for one of my Active Model Serializers to make sure that the JSON output is what I expect. However, I cannot figure out why RSpec is parsing my 'expected' output to leave out my array of testjobs, and I do not understand why I cannot get 'expected' and 'got' outputs to equal each other. At one point, I even copy-pasted the 'got' result to my 'expected' input and still received a failure message that the two strings were not equal. However, when I compared those two strings in REPL using ==, the output was true. How do I resolve these issues to get an effective test? RSpec Error: Failures: 1) TestrunSerializer creates special JSON for the API Failure/Error: expect(serializer.to_json).to eq('{"testrun":{"id":1,"run_at":null,"started_at":null,"state":"pending","completed_at":null,"testjobs":[{"id":2,"active":false,"testchunk_id":2,"testrun_id":1,"testchunk_name":"flair","testchunk":{"id":15,"name":"flair"}}],"branch":{"id":1,"name":"dev","repository":{"id":321,"url":"fakeurl.com"}}}}') expected: "{\"testrun\":{\"id\":1,\"run_at\":null,\"started_at\":null,\"state\":\"pending\",\"completed_at\":nu...r\"}}],\"branch\":{\"id\":1,\"name\":\"dev\",\"repository\":{\"id\":321,\"url\":\"fakeurl.com\"}}}}" got: "{\"testrun\":{\"id\":1,\"run_at\":null,\"started_at\":null,\"state\":\"pending\",\"completed_at\":nu...s\":[],\"branch\":{\"id\":1,\"name\":\"dev\",\"repository\":{\"id\":321,\"url\":\"fakeurl.com\"}}}}" (compared using ==) # ./spec/serializers/testrun_spec.rb:11:in `block (2 levels) in <top (required)>' Finished in 0.79448 seconds (files took 5.63 seconds to load) 1 example, 1 failure Failed examples: rspec ./spec/serializers/testrun_spec.rb:8 # TestrunSerializer creates special JSON for the API Here is the RSpec test: require 'rails_helper' describe TestrunSerializer, type: :serializer do let(:repo) { Repository.create(id: 321, url: "fakeurl.com") } let(:branch) { Branch.create(id: 1,name: "dev", repository_id: repo.id) } let(:testchunk) { Testchunk.create(id: 15, name: "flair") } it "creates special JSON for the API" do serializer = TestrunSerializer.new Testrun.new("id":1, name: "name", "run_at": nil, state: "pending", branch_id: branch.id) testjob = Testjob.create(id: 8, active: false, testchunk_id: testchunk.id, testrun_id: 1) expect(serializer.to_json).to eq('{"testrun":{"id":1,"run_at":null,"started_at":null,"state":"pending","completed_at":null,"testjobs":[{"id":2,"active":false,"testchunk_id":2,"testrun_id":1,"testchunk_name":"flair","testchunk":{"id":15,"name":"flair"}}],"branch":{"id":1,"name":"dev","repository":{"id":321,"url":"fakeurl.com"}}}}') end end Here is the actual serializer: class TestrunSerializer < ActiveModel::Serializer attributes :id, :run_at, :started_at, :state, :completed_at, :testjobs has_many :testjobs has_one :branch end Technologies used: Rails 5.1, RSpec 3.6, Ruby 2.4
It looks like your testjobs are not matching completed_at\":nu...r\"}}],\"branch\" vs completed_at\":nu...s\":[], You should set up your specs so the testjobs are returned as well. Please note that the diff string is cut in the middle - this is one of the most annoying part of eq matcher when used with strings. Edit: You may wont to switch to comparing arrays/hashes instead of strings to get better diffs. expect(serializer).to eq {testrun: "..."} (Drop to_json in your assertions)
The reason why your test didn't pass is trivial: inside the it block, you assigned the Testrun id (1) while creating the Testjob record, but the Testrun record does not exist. SomeActiveRecord.new() will not create any actual record until you invoke save() on it, or you can just invoke SomeActiveRecord.create for that. some_active_record = SomeActiveRecord.new(...) some_active_record.save # or some_active_record = SomeActiveRecord.create(...) So the final solution may look something like: it "creates special JSON for the API" do testrun = Testrun.create(id: 1, name: "name", run_at: nil, state: "pending", branch_id: branch.id) serializer = TestrunSerializer.new(testrun) testjob = Testjob.create(id: 8, active: false, testchunk_id: testchunk.id, testrun_id: testrun.id) expect(serializer.to_json).to eq('{"testrun":{"id":1,"run_at":null,"started_at":null,"state":"pending","completed_at":null,"testjobs":[{"id":2,"active":false,"testchunk_id":2,"testrun_id":1,"testchunk_name":"flair","testchunk":{"id":15,"name":"flair"}}],"branch":{"id":1,"name":"dev","repository":{"id":321,"url":"fakeurl.com"}}}}') end Improvement Scope: Please have a look at the tests for :json adapter in the active_model_serializers repo: https://github.com/rails-api/active_model_serializers/blob/v0.10.6/test/action_controller/json/include_test.rb. You can easily convert the tests to suite with rspec. If you want to test the json output, then you should put the tests under controller or request specs; rather than in serializers. Because rendering json is the responsibility of the adapter; serializers merely feed the adapter with all the attributes and associations defined in them.
Working solution: I added the line serializer.testjobs << testjob to explicitly associate the testjob with the object, and the test now passes.
RSpec eq matcher returns failure when two things are equal
In my controller test, I am testing the correct value is assigned to an instance variable. When I do expect(assigns(:conversations)).to eq #user_inbox RSpec tells me: Failure/Error: expect(assigns(:conversations)).to eq #user_inbox expected: #<ActiveRecord::Relation [#<Mailboxer::Conversation id: 4, subject: "Dude, what up?", created_at: "2014-10-21 08:43:50", updated_at: "2014-10-21 08:43:50">]> got: #<ActiveRecord::Relation [#<Mailboxer::Conversation id: 4, subject: "Dude, what up?", created_at: "2014-10-21 08:43:50", updated_at: "2014-10-21 08:43:50">]> (compared using ==) Diff: I see that there is no difference between the expected and the actual. I would like to know what is causing this test to fail.
ActiveRecord::Relation compares based on the actual relation, not the result set. For example, User.where(:id => 123) == User.where(:email => "fred#example.com") will return false, even the query results are both the same, since the actual queries are different. I suspect that you care much more about the query results rather than how it was composed, in which case you can use to_a to convert the relation to an array of active record objects. Note that Active Record defines equality based only on the value of the id attribute (with a special case for unsaved objects).
Yes, because this is two ActiveRecord::Relation object. Your instance variable is the first one and you create another one called conversations You should test the number of rows or other property with something like this: expect(assigns(:conversations).count).to eq #user_inbox.count
Maybe you should change the test strategy. When your test is hard to write your code is wrong or your test strategy is wrong. I recommend you no test query result in the controller's test. you should mock your query result describe 'GET user conversations' do before do your_user.stub(:conversations).and_return "foo bar" end it 'assigns the conversations of the user' do get :user_conversation expect(assigns(:conversations)).to eq your_user.conversations end end or you should test that some_collaborator.should_receive(:some_methods) describe 'GET user conversations' do before do some_collaborator.stub(:conversations) end it 'assigns the conversations of the user' do some_collaborator.should_receive(:conversations) get :user_conversation end end
RSpec - Date should be between two dates
How can I test a date to see if it's between two dates? I know I can do two greater-than and less-than comparisons but I want an RSpec method to check the "betweeness" of the date. For example: it "is between the time range" do expect(Date.now).to be_between(Date.yesterday, Date.tomorrow) end I tried expect(range).to cover(subject) but no luck.
Date.today.should be_between(Date.today - 1.day, Date.today + 1.day)
Both of the syntaxes you wrote are correct RSpec: it 'is between the time range' do expect(Date.today).to be_between(Date.yesterday, Date.tomorrow) end it 'is between the time range' do expect(Date.yesterday..Date.tomorrow).to cover Date.today end If you are not using Rails you won't have Date::yesterday or Date::tomorrow defined. You'll need to manually adjust it: it 'is between the time range' do expect(Date.today).to be_between(Date.today - 1, Date.today + 1) end The first version works due to RSpec's built in predicate matcher. This matcher understand methods being defined on objects, and delegates to them as well as a possible ? version. For Date, the predicate Date#between? comes from including Comparable (see link). The second version works because RSpec defines the cover matcher.
I didn't try it myself, but according to this you should use it a bit differently: it "is between the time range" do (Date.yesterday..Date.tomorrow).should cover(Date.now) end
You have to define a matcher, check https://github.com/dchelimsky/rspec/wiki/Custom-Matchers It could be RSpec::Matchers.define :be_between do |expected| match do |actual| actual[:bottom] <= expected && actual[:top] >= expected end end It allows you it "is between the time range" do expect(Date.now).to be_between(:bottom => Date.yesterday, :top => Date.tomorrow) end
how to check/test specific field in json using rspec
This is my rspec code:- it "which has max value" do get :index, Devise.token_authentication_key => #user.authentication_token, business_id: #business.id, max: '1' expect(request.flash[:alert]).to eq(nil) expect(response.body).to eq([#location].to_json(LocationFinder::API_PARAMS.merge(:root => false))) end and testing result is- expected: "[{\"address\":\"1120 Milky Way\",\"business_id\":1,\"city\":\"Cupertino]" got: "[{\"address\":\"1120 Milky Way\",\"business_id\":1,\"city\":\"Cupertino,\"distance\":260.33452958767384,]" Here Distance is an extra field , how can i check particular fields or if it is not possible , how to eliminate "distance" field which is not check by rspec.
You could check individual fields using something like: # get the first entry in the JSON array json_response = JSON.parse(response.body).first # compare each field expect(json_response['address']).to eq(#location.address) expect(json_response['business_id']).to eq(#location.business_id) expect(json_response['city']).to eq(#location.city) Of course you may need to adjust the exact methods you call on #location depending on your implementation, but that's the gist of it.