RSpec Rails - subject + assigns as one liner? - ruby-on-rails

I am struggling with the following RSpec. Why does this works:
it 'GET articles#new creates new instance of Article' do
get :new
expect(assigns[:article]).to be_a(Article)
end
And neither of those do (found some examples with different brackets and that's why I decided to check both possibilities)
subject { get :new }
it { expect(assigns[:article]).to be_a(Article) }
it { expect(assigns(:article)).to be_a(Article) }
I am getting this error:
Failure/Error: it { expect(assigns(:article)).to be_a(Article) }
expected nil to be a kind of Article(id: integer, title: string, body: string, author_id: integer, created_at: datetime, updated_at: datetime)
# ./spec/controllers/articles_controller_spec.rb:35:in `block (4 levels) in <top (required)>'
I don't know how to retrieve "article" from the subject...
I have also tried some various combinations of "subject" and "assigns" inside it { ... }, but I couldn't get it working.
I'd rather keep it clean and store it in one line only :)
BTW: Do you have any other habits of writing Specs for controllers? (I already check for response 200)

According to the documentation, a controller spec :
allows you to simulate a single http request in each example, and then
specify expected outcomes such as:
instance variables assigned in the controller to be shared with the view
that's why you have to run the request (like get :new) inside the it block to return the correct output.
If you define the request as the subject of your spec (precision about subject here), you need to run the subject inside the it block :
subject { get :new }
it 'GET articles#new creates new instance of Article' do
subject
expect(assigns[:article]).to be_a(Article)
end

subject, like let is lazy-evaluated only when you are referencing it. You can use subject! instead, which will be evaluated before each example

Related

TypeError during assert_no_difference test

The issue
I am writing unit tests for a RoR API, and I would like to test some entries from the response's body (something I have already done for another controller).
Basically, for an index method, I wanna check that every sub-element included in data share the same id as a specific one. Here is the complete test for the index method :
test "should access Membership index - specifix team" do
get api_v1_team_memberships_url(#team),
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
assert_response :success
json_response = JSON.parse(self.response.body)
assert_no_difference #membership.member_id, json_response['data']['attributes']['member_id']
end
But whatever I try, I get the very same error stack saying I have an issue with data type :
Error:
Api::V1::MembershipsControllerTest#test_should_access_Membership_index_-_specifix_team:
TypeError: no implicit conversion of String into Integer
test/controllers/api/v1/memberships_controller_test.rb:20:in `[]'
test/controllers/api/v1/memberships_controller_test.rb:20:in `block in <class:MembershipsControllerTes
t>'
I really don't get why, since, as I said above, that's a check I've already done for some other controllers.
assert_equal #task.activity_id, json_response['data']['attributes']['activity_id']
What I have tried so far
convert both element from the assert_no_difference function to string (to_s) or to integer (to_i)
use assert_equal instead of assert_no_difference
The error you are getting
TypeError: no implicit conversion of String into Integer
is commonly caused by trying to get a value from an array by a string index. For example, the following will raise the same error
a = [1, 2]
puts a['test']
In your case, I suspect json_response or json_response['data'] is an array.
Also, I believe you want to use assert_equal instead of assert_no_difference. assert_no_difference takes a numeric expression and a block; and asserts the expression is the same before and after the block is executed. https://api.rubyonrails.org/v7.0.2.2/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_no_difference

What should I use in place of "assigns" when using RSpec with Rails 5+?

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

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.

Ruby - passing block inside the function parenthesis

I have recently started to learn Ruby on Rails and it is really weird to get used to the syntax of Ruby.
I decided to go with all the parenthesis (that I know from other languages) that can be placed and I got stuck:
test "invalid signup information" do
get signup_path
assert_no_difference("User.count", {
user_params = { user: {
name: "",
email: "foo#invalid",
password: "foo",
password_confirmation: "bar"
}}
post(user_path, {params: user_params})
})
end
I want to pass a block into the assert_no_difference and somehow it is showing me an error during my tests. It started to show it after I places the definition of user_params. As far as I read some websites the syntax is OK, so what might be going wrong?
There's two general forms for passing in blocks. The long-form way is to use do ... end:
assert_no_difference('User.count') do
# ...
end
There's also the curly brace version:
assert_no_difference('User.count') {
# ...
}
Note that the curly brace style is generally reserved for single line operations, like this:
assert_no_difference('User.count') { post(...) }
For multi-line you generally want to use do...end since it's easier to spot. The When in Rome principle applies here, so you may need to shed some of your expectations in order to do things the Ruby way.
What you're doing wrong is passing in an argument that's presumed to be a Hash, but as it contains arbitrary code that's invalid. Unlike JavaScript the block is defined outside of the arguments to function call.
Cleaning up your code yields this:
test "invalid signup information" do
get signup_path
assert_no_difference("User.count") do
post(user_path,
params: {
user: {
name: "",
email: "foo#invalid",
password: "foo",
password_confirmation: "bar"
}
}
)
end
end
Note you can supply the arguments inline, plus any hash-style arguments specified as the last argument in a method call does not need its curly braces, they're strictly optional and usually best omitted.

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

Resources