Mathieu Mathieu - 6 months ago 117
Ruby Question

Rails 4/ Factory Girl - has_many/has_many factory and defining different objects

I'd like to define a factory for a has_many/has_many relationship (which I think I got it right) but I don't know how then to define attributes for each objects of these created factory.

Here is the homepage, it's a list of cards of deals

We assume the homepage view below is for a SIGNED-IN user (note current_user comes from Devise).

<% @deals.each do |deal| %>
@userdeal = UserDeal.where('user_id = ? AND deal_id = ?', current_user.id, deal.id).take

<div>
<div class="button">
<% if @userdeal.number_of_clicks = 4 %>you reached the maximum clicks
<% end %>
</div>
</div>
<% end %>


Basically if a user has for a specific deal (on the table user_deals, see below the models) a user_deal.number_of_clicks = 4 then, on the card a button will appear with a message like "you reached the maximum clicks". if nb of clicks <4, no button appears.

So I want to use the factory on a Feature test where I'll check that if I create with fatcory girl one object @userdeal1 where the user reached 4 clicks, on this card he sees the button and its text, but for other deals he sees nothing.

Here's what I have so far

models

class Deal < ActiveRecord::Base
has_many :user_deals, dependent: :destroy
has_many :users, through: :user_deals
end

class User < ActiveRecord::Base
has_many :user_deals
has_many :deals, through: :user_deals
end

class UserDeal < ActiveRecord::Base
belongs_to :user, :foreign_key => 'user_id'
belongs_to :deal, :foreign_key => 'deal_id'
end


And the structure of the table user_deal

# Table name: user_deals
#
# id :integer not null, primary key
# user_id :integer
# deal_id :integer
# number_of_clicks :integer default(0)
# created_at :datetime
# updated_at :datetime


So far I found how to create

FactoryGirl.define do
factory :user_deal do
association :user
association :deal

end


Following factorygirl ReadMe I created this:

FactoryGirl.define do
factory :user do

sequence(:email) { |n| "person#{n}@example.com"}
password "vddfdf"
password_confirmation "vddfdf"
confirmed_at Time.now
confirmation_token nil

factory :superadmin do
after(:create) {|user| user.add_role(:superadmin)}
end

after(:create) do |user|
user.deals << FactoryGirl.create(:deal)
end

factory :user_with_deals do
# used for defining user_deals
transient do
deals_count 5
end
after(:create) do |user, evaluator|
create_list(:deal, evaluator.deals_count, user: user)
end
end

end
end


But now I don't know how to say 'ok of one of these created user_deals in the factory, I'd like one to have number_of_clicks =4, and the other one number_of_clicks=1). so I tried to put this inside the test directly , as below:

describe 'HP deal card features', :type => :feature do

context "As signed-in USER" do
let(:subject) { ApplicationController.new }

before do
@user = FactoryGirl.create(:user, :user_country_name => 'Germany')
@deal1 = FactoryGirl.build( :deal)
@deal2 = FactoryGirl.build( :deal)
@user_deal_with_max_of_clicks = FactoryGirl.build( :user_with_deals,
:user => @user,
:deal => @deal1,
:number_of_clicks => 4
).save(validate: false)
@user_deal_with_clicks_available = FactoryGirl.build( :user_with_deals,
:user => @user,
:deal => @deal2,
number_of_clicks => 1 # still has clicks
).save(validate: false)
it "the right behavior for the buttons telling him how many clicks he has left" do

sign_in @user
visit root_path
# I'll find a way to test here if there is the text "you reached the maximum clicks" for the first card and not for the second
end
after do
visit destroy_user_session_path
end

end


But the test does not work and give me different type of errors according to the small change I try. I am pretty sure I don't manage to really create the 2 objects user_deals , one with number_of_clicks= 4 and the other one number_of_clicks= 1.

EDIT

After requests to post the errors:

If i leave inside the @user_deal_with_max_of_clicks " :user=> @user, and :deal => @deal1", I get

NoMethodError:
undefined method `user=' for #<User:0x0000000b4754e0>


and

NoMethodError:
undefined method `deal=' for #<User:0x0000000b451568>


But I remove them out as follows:

@user_deal_with_max_of_clicks = FactoryGirl.build( :user_with_deals,
:number_of_clicks => 4
).save(validate: false)


then I get this error:

NoMethodError:
undefined method `number_of_clicks=' for #<User:0x0000000b46ce80>


EDIT 2

utilities.rb

include ApplicationHelper

def sign_in(user)
visit new_user_session_path
fill_in I18n.t("formtastic.labels.user.email"), with: user.email
fill_in I18n.t("formtastic.labels.user.password"), with: user.password
click_on I18n.t("devise.sessions.login_page.login")
end

Answer

First off, I'd advise you to use let. There is a great guide to write cleaner and better specs.

Secondly, it is vital to set up good factories. They will simplify the testing process enormously:

So starting off, your user factories can be restructured to

FactoryGirl.define do
  factory :user do
    sequence(:email) { |n| "person#{n}@example.com" }

    password "vddfdf"
    password_confirmation "vddfdf"

    confirmed_at Time.now
    confirmation_token nil

    trait :superadmin do
      role :superadmin
    end

    trait :with_deals do 
      after(:create) do |user|
        create_list(:deal, 5, user: user)
      end
    end
  end
end

Traits are nice way to selectively adjust factories. So if you want your user to be a superadmin, now just use

create(:user, :superadmin)

Want a superadmin with deals? Easy.

create(:user, :superadmin, :with_deals)

You have not posted your deal factories, but I'm sure you can adapt these tricks to them as well.

Finally, leading to the user_deals. In your current factories, you don't address your number_of_clicks column.

Again, you can easily set this up with traits:

FactoryGirl.define do
  factory :user_deal do
    association :user
    association :deal

    trait :few_clicks do
      number_of_clicks 1
    end

    trait :many_clicks do
      number_of_clicks 4
    end
  end
end

Now to the spec itself, with the learnt tricks it's an easy task to set up your desired relations:

describe 'HP deal card features', :type => :feature do
  context "As signed-in USER" do
    let(:subject) { ApplicationController.new }
    let(:user) { create(:user, user_country_name: 'Germany')  }
    let(:deal1) { create(:deal) }
    let(:deal2) { create(:deal) }

    before do
      create(:user_deal, :few_clicks, user: user, deal: deal1)
      create(:user_deal, :many_clicks, user: user, deal: deal2)
    end

    it "tells the user how many clicks he has left" do
      sign_in user
      visit root_path
      # I'll find a way to test here if there is the text "you reached the maximum clicks" for the first card and not for the second
    end

    after do
      visit destroy_user_session_path
    end
  end
end
Comments