Zoinks10
Zoinks10

Reputation: 629

How should I set up complex Factories in Rails?

In my rails application I have three objects I would like to model for a particular Rspec test. These are Organizations, Users, and Sales Opportunities.

class Organization
  has_many :users
end

class User
  has_many :sales_opportunities
end

An Organization has to have a unique name, and for the authorisation/authentication aspects of my app to work, both users need to belong to the same Organization. For the purposes of this test I want one User to be admin and one to be non-admin.

This works successfully in other parts of my application (e.g. sign up - the first user is automatically the admin, subsequent users are not, and for viewing links etc on pages). However what I now need to do is create two users (both belong_to one organization), make one of them the admin, and then when I delete the sales_opportunity non-admin user any sales_opportunities that belonged to this non-admin user should transfer ownership to the admin (for re-allocation).

FWIW I am using associations for some of the other tests - but in this case I can't create two users associated with one organization, so I've used:

before(:create) do |organization|
    organization.users << FactoryGirl.build(:user ...admin params)
    organization.users << FactoryGirl.build(:user ...non_admin params)
end

in the factory for the organization to build 2 users who both belong to one organization and have different characteristics. This works well.

If I try to add another before(:create) to the factory to then build a sales_opportunity it fails (because the users aren't created yet I believe). After(:create) statements also fall flat. I've also tried defining the non_admin user in the tests themselves (e.g. by using a let(:non_admin) = Organization.users.last and then non_admin.sales_opportunities.create statement), but this fails to product any sales opportunities (not sure what it does).

Are there any good resources on how to build tests like this? I can write the code to solve the problems in no time - I seem to waste a huge amount of time writing the tests first. I'm sure these are not particularly DRY either looking through my factories.

Any help would be appreciated.

Upvotes: 0

Views: 778

Answers (2)

Surya
Surya

Reputation: 16022

in rspec/factories/sales_opportunities.rb:

FactoryGirl.define do
  sequence(:name) { |n| "Sales Oppotunity - #{n}" }

  factory :sales_opportunity do
    user
    name { generate(:name) }
    # add more attributes here
  end
end

in rspec/factories/users.rb:

FactoryGirl.define do

  factory :user do
    organization

    sequence(:name) { |n| "Test user #{n}" }
    sequence(:email) { |n| "test.user#{n}@example.com" }
    password "password"

    admin false
    # add more attributes here
  end

  factory :admin_user, parent: :user do
    admin true
  end

  factory :user_with_sales_opportunities, parent: :user, traits: [:with_sales_opportunity]

  factory :admin_user_with_sales_opportunities, parent: :admin_user, traits: [:with_sales_opportunity]

  trait :with_sales_opportunity do
    ignore do # instead of ignore use transient if you're using factory girl 4.3 or above
      sales_opportunities_count 5
    end

    after(:create) do |user, evaluator|
      sales_opportunities { FactoryGirl.create_list(:sales_opportunity, evaluator.sales_opportunities_count, user: user) }
    end
  end
end

in rspec/factories/organizations.rb:

FactoryGirl.define do
  sequence(:name) { |n| "Organization - #{n}" }

  factory :organization do
    name { generate(:name) }

    ignore do
      users_count 5
      admin_users_count 2
      sales_opportunities_count 5
    end

    after(:create) do |organization, evaluator|
      users { FactoryGirl.create_list(:user_with_sales_opportunities, evaluator.users_count, organization: organization, sales_opportunities_count: sales_opportunities_count) + FactoryGirl.create_list(:admin_user_with_sales_opportunities, evaluator.users_count, organization: organization, sales_opportunities_count: sales_opportunities_count) }
    end
    # add more attributes here
  end
end

Now you can:

let(:organization) { FactoryGirl.create(:organization, users_count: 4, admin_users_count: 3, sales_opportunities_count: 4) }

Upvotes: 1

Allan W Smith
Allan W Smith

Reputation: 756

Im not sure exactly what you're trying to do, as you seem to be deleting the sales opportunity that you want to transfer?

If you think its an rspec you could instead try using after(:build)

Something like:

factory :user, class: User do
  sequence(:email) { |n| "user#{n}@example.org" }
  password "foobar"
  # Standard user stuff etc

  trait :admin do
    after(:build) do |user|
      user.set_admin
    end
  end
end

factory :organisation, class: Organisation do
  name "Super Duper"

  after(:build) do |organization|
    organization.users << FactoryGirl.build(:user)
    organization.users << FactoryGirl.build(:user, :admin)
  end
end

I feel like you may have a modeling issue though. Shouldn't your sales opportunities be standalone?

ie:

class SalesOpportunity
  field :name, type: String
  field :phone, type: Integer

  belongs_to :user  
end

class User
  field :name, type: String
  field :phone, type: Integer

  has_many :salesopportunities
end

This way you can delete a user object and persist the sale opportunity?

Upvotes: 0

Related Questions