Search code examples
ruby-on-railsrspec2ruby-on-rails-4rspec-rails

Rspec2 + Rails4: Testing for displayed model fields in form partial


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


Solution

  • 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.