Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test forms with nested attributes using RSpec?

I have an Invoice model that may contain a number of Items:

class Invoice < ActiveRecord::Base

  attr_accessible :number, :date, :recipient, :items_attributes

  belongs_to :user

  has_many :items

  accepts_nested_attributes_for :items, :reject_if => :all_blank, :allow_destroy => true

end

I am trying to test this using RSpec:

describe InvoicesController do

  describe 'user access' do

    before :each do
      @user = FactoryGirl.create(:user)
      @invoice = @user.invoices.create(FactoryGirl.attributes_for(:invoice))
      sign_in(@user)
    end

    it "renders the :show view" do
      get :show
      expect(response).to render_template :show
    end

  end

end

Unfortunately, this test (and all the others) fail with this error message from RSpec:

Failure/Error: @invoice = @user.invoices.create(FactoryGirl.attributes_for(:invoice))
ActiveModel::MassAssignmentSecurity::Error:
Can't mass-assign protected attributes: items

How can I create an invoice with items that will pass my tests?

I am using FactoryGirl to fabricate objects like this:

factory :invoice do
  number { Random.new.rand(0..1000000) }
  recipient { Faker::Name.name }
  date { Time.now.to_date }
  association :user
  items { |i| [i.association(:item)] } 
end

factory :item do
  date { Time.now.to_date }
  description { Faker::Lorem.sentences(1) }
  price 50
  quantity 2
end
like image 353
Tintin81 Avatar asked May 21 '26 09:05

Tintin81


2 Answers

-To use nested attributes in your example you need to pass in "item_attributes" and not "items" like you are currently doing.

I'm not fluent in FactoryGirl, but maybe something along these lines would work? :

invoice_attributes = FactoryGirl.attributes_for(:invoice)
invoice_attributes["item_attributes"] = invoice_attributes["items"]
invoice_attributes["items"] = nil
@invoice = @user.invoices.create(invoice_attributes)

That s should hopefully simulate what parameters get passed in from your form.

like image 68
cmrichards Avatar answered May 23 '26 23:05

cmrichards


Edit: Misunderstood the question. Apologies.

Instead of

before :each do
  @user = FactoryGirl.create(:user)
  @invoice = @user.invoices.create(FactoryGirl.attributes_for(:invoice))
  sign_in(@user)
end

Simply create the factories for invoice passed with a user param, like so:

before :each do
  @user = FactoryGirl.create(:user)
  FactoryGirl.create :invoice, user: @user
  sign_in(@user)
end

Also, this a minor style suggestion, but instead of instance variables, you can use lets, like so:

let(:user) { FactoryGirl.create :user }

before :each do
  FactoryGirl.create :invoice, user: user
  sign_in(user)
end

Passing 'user' in to the invoice create will also create the user (and it is callable as simply 'user').

Minor caveat: I've been doing this for about 6 months, so there might be someone more knowledgeable who disagrees with my style suggestion.

like image 44
Mike Manfrin Avatar answered May 23 '26 23:05

Mike Manfrin



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!