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.
Related
I'm currently testing the index action of one my controllers in the form of a request spec and want to make sure that the object collection is being passed through. So far I've consulted this post for guidance:
it "populates an array of contacts starting with the letter" do
smith = FactoryBot.create(:contact, lastname: 'Smith')
jones = FactoryBot.create(:contact, lastname: 'Jones')
get :index, letter: 'S'
expect(assigns(:contacts)).to match_array([smith])
end
Unfortunately the above example will thrown this error:
NoMethodError: assigns has been extracted to a gem. To continue using it, add `gem 'rails-controller-testing'` to your Gemfile.
I'd simple like to know what I would use in favor of assign in this case? I've looked high and low for an example for this new methodology but came up short.
Reference
rails-controller-testings - assigns
Just test the output of the controller instead of poking your fingers into the internals. Better yet don't use controller specs - use request or feature specs instead.
The output of the controller is the response object which contains headers and the response body.
So for example if you're testing an API in a request spec you could test the parsed json in the response body:
it "returns an array of contacts starting with the letter" do
smith = FactoryBot.create(:contact, lastname: 'Smith')
jones = FactoryBot.create(:contact, lastname: 'Jones')
get :index, letter: 'S'
last_names = parsed_response["contacts"].map { |c| c["lastname"] }
expect(last_names).to include 'Smith'
expect(last_names).to_not include 'Jones'
end
I just added a Postgres json type to a Rails/Active Record table I'm working with.
I'd like to populate a record with a default value in Rails fixtures:
fixture_id:
existing_column: "foobar"
newly_added_column: <%= JSON.dump({:reason => 'foobar'}) %>
Previously, I'd stored stringified JSON this way in a text column. However, when I now run this unit test:
test "my test" do
sut = fixtures(:fixture_id)
assert_not_nil sut.newly_added_column
end
The test fails. Because it is JSON at the database level, I don't think it's useful to dump it to a string, but the YAML fixtures can't seem to keep an object as a Hash (when I try it without the JSON.dump, I get ActiveRecord::Fixture::FormatError: a YAML error occurred parsing).
Mind you, I am using Rails 3, so I think some of the support for this may be in Rails 4, but in Rails 3, the migration to add a json Postgres column type still work.
I believe your fixture can be as simple as:
fixture_id:
existing_column: foobar
newly_added_column: {'reason':'foobar'}
To avoid having lengthy JSON structure inline in your fixtures, YAML provide a useful > operator
fixture_id:
existing_column: "foobar"
data: >
{
"can_edit": true,
"can_se": true,
"can_share": true,
"something_else": true
}
In Rails 5 it is possible to simply describe the JSON in YAML format. It will then be converted into correct JSON when fixtures are loaded in the DB:
parent:
name: A fixture
json_field:
- name: JSON Object 1
foo: bar
- name: JSON Object 2
foo: baz
(tested with JSONB attributes in Postgres)
If you are using ActiveRecord to generate content for your fixtures, you have to ensure that it converts attribute values to json. If you rely on the default serialization, it will produce hash rocket text which is invalid YAML.
For example, I have a legacy database that I am using to create fixtures for my new application (relevant code listed below):
presenter.rb
module FixturePresenter
def replacer(str)
str.downcase.tr(' ', '_').tr('-', '_')
end
# rubocop:disable Metrics/AbcSize
def plan
tag = replacer(name) + '_' + replacer(type)
<<~FIXTURE
#{tag}:
name: #{name}
type: #{type}
address: #{address}
FIXTURE
end
end
runner.rb
...
Legacy::Provider.all.each_with_index do |p, i|
p.extend(FixturePresenter)
f.puts p.plan
end
...
The address column in the plan table is a JSONB datatype. When we run this code this is the resulting YAML:
abc_plan_la_jolla:
name: ABC Plan La Jolla
type: default
address: {"street"=>"9888 Genesee Ave","unit"=>"","city"=>"La Jolla","state"=>"CA","postal_code"=>"92037"}
When you run your test you will get the very sad Syck error:
Psych::SyntaxError: (<unknown>): did not find expected ',' or '}' while parsing a flow mapping at line 4 column 12
The following change to the code will produce the correct YAML:
presenter.rb
address: #{address} to address: #{address.to_json}
The above change will produce a happy fixture definition:
abc_plan_la_jolla:
name: ABC Plan La Jolla
type: default
address: {"street":"9888 Genesee Ave","unit":"","city":"La Jolla","state":"CA","postal_code":"92037"}
Try
fixture_id:
existing_column: "foobar"
newly_added_column: "{\"reason\": \"foobar\"}"
I was maintaining a legacy Rails 4.2 application and needed to put JSON store value into a fixture. I made the following monkey patch to fix the problem. Hope this helps someone:
module ActiveRecord
class Fixture
def to_hash
h = fixture
model_class.attribute_names.each do |name|
typedef = model_class.type_for_attribute(name)
h[name] = typedef.coder.dump(h[name]) \
if typedef.is_a? ActiveRecord::Type::Serialized
end
h
end
end
end
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
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
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.