Stefan Hansch
Stefan Hansch

Reputation: 1590

Undefined method when creating an object FactoryGirl.

I write test for my model Poll. Check, creating object poll with nested atributes vote_option. Its my Factories.rb:

FactoryGirl.define do

  factory :vote_option do
    title "Ruby"
    poll
  end

  factory :poll do
    topic "What programming language are you using?"

    trait :vote_option1 do
      association :vote_option, title: "C#"
    end

    trait :vote_option2 do
      association :vote_option, title: "Ruby"
    end

    factory :poll_with_vote1, traits: [:vote_option1]
    factory :poll_with_vote2, traits: [:vote_option2]
  end
end   

I created test and check, that object was created. poll_srec.rb:

require 'rails_helper'

RSpec.describe Poll, type: :model do
  #let(:user) { FactoryGirl.create(:user) }

  before do
    @poll = FactoryGirl.create(:poll_with_vote1)

  end

  subject{@poll}

  it { should be_valid }

end

I run poll_spec.rb and givess an error: 1) Poll Failure/Error: @poll = FactoryGirl.create(:poll_with_vote1) NoMethodError: undefined method `vote_option=' for #

Why is this error? What is wrong with my factories?

Just in case model Poll:
class Poll < ActiveRecord::Base
  VOTE_OPTIONS_MIN_COUNT = 1

  has_many :vote_options, dependent: :destroy
  has_many :votes

  validates :topic, presence: true
  #validates :vote_options, presence: true #association_count: { minimum: VOTE_OPTIONS_MIN_COUNT }


  #validates :user_id, presence: true

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

  def normalized_votes_for(option)
    votes_summary == 0 ? 0 : (option.votes.count.to_f / votes_summary) * 100
  end

  def votes_summary
    vote_options.inject(0) { | summary, option | summary + option.votes.count  }
  end
end

Upvotes: 2

Views: 1915

Answers (1)

BroiSatse
BroiSatse

Reputation: 44675

The reason you're getting this error is because you do not have association with such a name. Try:

FactoryGirl.define do

  factory :vote_option do
     title "Ruby"
  end

  factory :poll do

    topic "What programming language are you using?"

    trait :vote_option1 do
      after(:create) {|poll| poll.vote_options << create(:vote_option, title: 'C#')         
    end

    trait :vote_option2 do
      after(:create) {|poll| poll.vote_options << create(:vote_option, title: 'Ruby')         
    end

    factory :poll_with_vote1, traits: [:vote_option1]
    factory :poll_with_vote2, traits: [:vote_option2]
  end
end

That being said, couple of personal practices:

  • Your tests are the documentation of your code. Anyone who is to work with that code in the future should be able to read your tests and to understand what given bit of code is supposed to do. Hence - your tests have to be extremely readable.

  • Try avoiding creating factories like, poll_with_vote1 - they are not helpful in understanding what is hapenning and you are loosing the power of traits. Just use it like: create :poll, :with_vote_option_1. It will also allow you to use both traits at the same time with create :poll, :with_vote_option_1, :with_vote_option_2 (ignoring issues with those traits for now, see next point)

  • Traits are to make your factories more clear. Good example might be a trait published wrapping logic of status 1 for enum field. Your traits are actually hiding from anyone reading your tests the actual hardcoded value of the vote option, which will produce Why the hell is he expecting 'C#' here?. There are better ways to do this.

  • It is good to randomize your factories rather then relying on hardcoded values. It will force you to write clearer tests. For example test expect(page).to have_content('Ruby') is much less self-explanatory than expect(page).to have_content(vote_option.title). You can't do first option with random factories. Also your test will suddenly get useless when you change the page to have title 'Ruby' - which again cannot happen with random data (Well, it can, but it will rather pass randomly with a very small probability of missing the bug)

  • You should always try to make your factories valid. In your example, factory poll is not.

This is how I would write factories for your models (using FFaker gem to randomize data)

FactoryGirl.define do

  factory :vote_option do
     title { FFaker::Lorem.word }
  end

  factory :poll do

    topic { FFaker::Lorem.sentence + '?' }

    transient do    # In old version `ignore do`
      vote_options_number { rand(1..4) }
    end

    after(:build) do |poll, ev|
      poll.vote_options << build_list :vote_option, ev.vote_options_number
    end

end

Upvotes: 3

Related Questions