Question
I'd like to write tests that check the model fields that are displayed in my "show" and "form" partials. I succeeded for "show", not for "form".
Main constrain: The solution must be able to loop through an Array that contains each names of the model fields.
I believe this case can be interesting for anyone that is trying to shorten his test script files, while having many fields, and having a complete control over what's displayed and what's not, so I'll put some efforts trying to find a solution, with your help if you please :)
Form view
Nothing fancy
= form_for @user do |f|
= f.select :field_1, options_from_collection_for_select ...
= f.text_field :field_2
...
Actual situation
I found an easy way for the "show" partial, here is how my spec file looks like:
def user_fields_in_show_view
[:field_1, :field_2, ..., :field_n]
end
it 'display fields' do
user_fields_in_show_view.each do |field|
User.any_instance.should_receive(field).at_least(1).and_call_original
end
render
end
This works well.
-
But the exact same technique does not work in the "form" partial, using the same code
def user_fields_in_form_view # List of fields need to be different, because of the associations
[:field_1_id, :field_2, ..., :field_n]
end
it 'display fields' do
user_fields_in_form_view.each do |field|
User.any_instance.should_receive(field).at_least(1).and_call_original
end
render
end
It whines like this:
Failure/Error: Unable to find matching line from backtrace
Exactly one instance should have received the following message(s) but didn't: field1_id, field_2, ..., field_n
# Backtrace is long and shows only rspec/mock, rspec/core, rspec/rails/adapters, and spork files
What I tried so far
1- I commented out the stub part of my tests and output rendered
to the console, to manually check what's generated by my view, and yes the fields are correctly generated.
2- I replaced User.any_instance
by the model I assign to the view, error is slightly different but it still not working
it 'display fields' do
user = create :user
assign :user, user
user_fields_in_form_view.each do |field|
user.should_receive(field).at_least(1).and_call_original
end
render
end
Gives:
Failure/Error: user.should_receive(field).at_least(1).and_call_original
(#<User:0x0000000506e3e8>).field_1_id(any args)
expected: at least 1 time with any arguments
received: 0 times with any arguments
3- I change the code so the it
is inside the loop, like this:
user_fields_in_form_view.each do |field|
it 'display fields' do
user = create :user
assign :user, user
user.should_receive(field).at_least(1).and_call_original
render
end
end
Same result as above
And I run out of options. I suspect the internals of FormBuilder to play a bad trick on me but I can't figure it out, I'm not very knowledgeable with those yet. Thanks for reading
I usually try to write unit test as simple as possible. Loops in unit tests don't add much readability and are not very good practice in general. I'd rewrite the test like this:
it 'should display user name and email' do
# note: `build` is used here instead of `create`
assign :user, build(:user, first_name: 'John', last_name: 'Doe', email: '[email protected]')
render
rendered.should have_content 'John'
rendered.should have_content 'Doe'
rendered.should have_content '[email protected]'
end
Thus, we're not limiting the view in how it should render the first and the last name. For example, if our view uses the following (bad) code in order to render user's full name, then your test will fail, but my test will work just fine, because it tests the behaviour of the view, not its internals:
<%= user.attributes.values_at('first_name', 'middle_name').compact.join(' ') %>
Moreover, multiple assertions in one test is a bad smell too. Going one step further, I'd replace this test with three smaller ones:
it "should display user's first name" do
assign :user, build(:user, first_name: 'John')
render
expect(rendered).to include 'John'
end
it "should display user's last name" do
assign :user, build(:user, last_name: 'Doe')
render
expect(rendered).to include 'Doe'
end
it "should display user's email" do
assign :user, build(:user, email: '[email protected]')
render
expect(rendered).to include '[email protected]'
end
========
UPD: Let's make it more dynamic in order to avoid tons of repetition. Tis doesn't answers why your spec fails, but hopefully represents working tests:
%i(first_name last_name email).each do |field|
it "should display user's #{field}" do
user = build(:user)
assign :user, user
render
expect(rendered).to include user.public_send(field)
end
end
In order to make these tests more reliable, make sure that user factory doesn't contain repetitive data.