FactoryGirl nested transient has_many association with validation - ruby-on-rails

My problem is that of FactoryGirl has_many association with validation, with the added complexity of the association being a "transient" attribute, and is quite nested
ie. my classes are (I'm using mongoid, assume Mongoid::Document is included in all models)
class User
has_many :company_admin_profiles
end
class CompanyAdminProfile
belongs_to :company
belongs_to :user # Cannot exist standalone
end
class Company
has_many :company_admin_profiles
validate :has_at_least_one_admin
end
So far with FactoryGirl I've written
FactoryGirl.define do
factory :user do
...
trait(:company_admin) do
transient do
company_admins_count 1
company { create(:company, admins_count: 0) }
end
after(:build) do |user, evaluator|
create_list(:company_admin_profile, evaluator.company_admins_count,
company: evaluator.company,
user: user,
first_name: user.first_name,
last_name: user.last_name,
email: user.email)
end
end
factory :company_admin_user, traits: [:company_admin]
end
end
FactoryGirl.define do
factory :company_admin_profile, class: Company::Admin do
company
end
end
FactoryGirl.define do
factory :company do
transient do
admins_count 1 # need one admin to pass validation
end
after(:build) do |company, evaluator|
build_list(:company_admin_user, evaluator.admins_count,
company: company)
end
end
end
I've tried several variations on this, the last error is
* company - Attribute already defined: company (FactoryGirl::AttributeDefinitionError)
* company_admin_profile - Attribute already defined: company (FactoryGirl::AttributeDefinitionError)
* company_admin_user - Attribute already defined: company (FactoryGirl::AttributeDefinitionError)

I just saw an upvote on the question, so maybe someone is actually interested in the solution I have for now.
I don't remember exactly where the problem was, but here's a code that works :
# factories/company.rb
FactoryGirl.define do
factory :company do
...
transient do
admins_count 1 # need one admin to pass validation
end
after(:build) do |comp, evaluator|
if evaluator.admins_count == 1
build(:company_admin_user, company: comp)
end
end
end
end
# factories/user.rb
FactoryGirl.define do
factory :user do
...
after(:build) do |user|
user.skip_confirmation!
end
# Company admin
trait(:company_admin) do
transient do
company_admins_count 1
company { build(:company, admins_count: 0) }
end
after(:build) do |user, evaluator|
create_list(:company_admin_profile, evaluator.company_admins_count,
company: evaluator.company,
user: user)
evaluator.company.save
end
end
factory :company_admin_user, traits: [:company_admin]
end
end
# factories/company_admin_profile.rb
FactoryGirl.define do
factory :company_admin_profile, class: CompanyAdminProfile do
...
company
end
end

Related

How to specify the association with factory_bot?

For example I have two models a user and a post. A post belongs_to a user and a user has many posts
#spec/factories/post.rb
FactoryBot.define do
factory :post do
user
body Faker::Movie.quote
posted_at "2018-04-03 13:33:05"
end
end
#spec/factories/user.rb
FactoryBot.define do
factory :user do
first_name 'Jake'
end
end
Using Rspec in a test I want to do this:
user = create(:user, first_name: 'Barry') #id 1
post = create(:post, user: user)
I would expect that the user_id of post to be 1 however it is creating another user prior and the user_id is 2.
How can you specify the association when you are creating the object with factory_bot / factory_girl?
You should use explicit associations instead of implicit ones:
#spec/factories/post.rb
FactoryBot.define do
factory :post do
association :user # <<<--- here the change
body Faker::Movie.quote
posted_at "2018-04-03 13:33:05"
end
end
#spec/factories/user.rb
FactoryBot.define do
factory :user do
first_name 'Jake'
end
end
https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#associations
Another option is to use #trait method within the parent.
FactoryBot.define do
factory :post do
user nil
body Faker::Movie.quote
posted_at "2018-04-03 13:33:05"
end
end
FactoryBot.define do
factory :user do
first_name 'Jake'
end
trait :with_post do
after(:create) do |user|
create(:post, user_id: user.id)
end
end
end
FactoryBot.create(:user, :with_post)
Here we have another solution in case your association name and factory name is different then you can follow below syntax.
#spec/factories/post.rb
FactoryBot.define do
factory :post do
association :author, factory: :user
body Faker::Movie.quote
posted_at "2018-04-03 13:33:05"
end
end
in case your factory name is author and model name is user then you can use above syntax

FactoryGirl : 'Factory not registered' error when added a factory to a has_many relation

I've got 2 factories files
spec/factories/cars.rb
spec/factories/users.rb
A user can have many cars and I would like to create a special trait for this case.
Into my car factory :
FactoryGirl.define do
factory :car do
...
trait :is_blue do
color 'blue'
end
end
factory :blue_car, parent: :car do
is_blue
end
end
My user factory
FactoryGirl.define do
factory :user do
...
trait :with_cars do
cars [ FactoryGirl.create(:blue_car) ]
end
end
factory :user_with_cars, parent: :user do
with_cars
end
end
When I want to use the 'user_with_cars' factory into my 'user_spec' file I've got a 'Factory not registered: blue_car' error
Example :
context 'with cars' do
subject { create(:user_with_cars) }
it 'should make some stuff' do
expect(subject.cars).not_to be_empty
...
end
end
I found a solution.
I change the way I create relations.
In the trait wich add relations I use the 'after(:create)' callback.
spec/factories/users.rb
FactoryGirl.define do
factory :user do
...
trait :with_cars do
after(:create) do |user|
user.cars << create(:blue_car)
end
end
factory :user_with_cars, parent: :user do
with_cars
end
end

Factory Girl - How to create a factory for a model that has associations?

I am attempting to create a factory for my user model, along with its associations. However, I cannot seem to get the syntax right in my Factory Girl code. I've read through the Factory Girl documentation but cannot seem to find any help with my specific use case. The errors I am currently receiving when I run my test suite are:
undefined method `subscription_args' for #<FactoryGirl::SyntaxRunner...
and
Trait not registered: valid_card_data
Here are my models and associations:
User.rb
has_one :subscription
has_one :plan, :through => :subscription
has_many :projects
Project.rb
belongs_to :user
Plan.rb
has_many :subscriptions
Subscription.rb
belongs_to :plan
belongs_to :user
and
And here is my Factory Girl code:
FactoryGirl.define do
factory :user do
first_name "Joel"
last_name "Brewer"
email { "#{first_name}.#{last_name}#example.com".downcase }
password "foobar"
password_confirmation "foobar"
user_type "entrepreneur"
subscription { build(:subscription, subscription_args) }
after(:create) do |user|
user.subscription.save!
end
end
factory :subscription do
user
plan_id '4'
## I am trying to access a helper method from support/utilities ##
## This call to valid_card_data doesn't seem to be working... ##
stripe_card_token valid_card_data
email "joel.brewer#example.com"
end
factory :project do
title "Sample Project"
user
end
end
Here's how I've done it in the past. Certainly not the only way:
(Note I am using cucumber.)
require 'factory_girl'
FactoryGirl.define do
factory :user do |f|
f.username 'superman'
end
factory :message do |f|
f.association :user
f.content 'Test message content'
end
end
This establishes that the message factory should associate the message to a user. Which user? I establish that at the point of use:
steps.rb:
Given(/^there is a user$/) do
#user = FactoryGirl.create(:user)
end
Given(/^the user has posted the message "(.*?)"$/) do |message_text|
FactoryGirl.create(:message, :content => message_text, :user => #user)
end
When(/^I visit the page for the user$/) do
visit user_path(#user)
end
Then(/^I should see "(.*?)"$/) do |text|
page.should have_content(text)
end
My approach, specifying at the point of use makes sense for this use case. e.g. Given is a user (user must be established first) and that user has posted a message (now the relationship between the existing user and the message can be established)...
That may or may not work out well for you, but it's how I've done it. This may or may not have helped you, but here's hoping.
There are several ways to do it. Here is one example:
after(:build) do |keyword, evaluator|
keyword.text = FactoryGirl.build(:keyword_text, :value => evaluator.keyword_text)
end
You dont need subscription_args - these can be set when you call the factory.
Where are you defining your trait?
In my factories they look like this:
trait :with_category_associations do
..
For more complicated relationships you probably want to use:
after(:create) do |keyword, evaluator|
evaluator.categories.each do |category|
FactoryGirl.create(:join_inventory_keyword, final: keyword, category: category)
end
end

Factory Girl and nested attributes validation error

I have a model Company which accepts nested attributes for Recruiters model. I need to have validation in my Company model that at least one recruiter was created during Company creation.
class Company < ActiveRecord::Base
has_many :recruiters, dependent: :destroy, inverse_of: :company
accepts_nested_attributes_for :recruiters, reject_if: ->(attributes) { attributes[:name].blank? || attributes[:email].blank? }, allow_destroy: true
validate { check_recruiters_number } # this validates recruiters number
private
def recruiters_count_valid?
recruiters.reject(&:marked_for_destruction?).count >= RECRUITERS_COUNT_MIN
end
def check_recruiters_number
unless recruiters_count_valid?
errors.add(:base, :recruiters_too_short, count: RECRUITERS_COUNT_MIN)
end
end
end
Validation works as expected but after adding this validation I have a problem with FactoryGirl. My factory for company looks like this:
FactoryGirl.define do
factory :company do
association :industry
association :user
available_disclosures 15
city 'Berlin'
country 'Germany'
ignore do
recruiters_count 2
end
after(:build) do |company, evaluator|
FactoryGirl.create_list(:recruiter, evaluator.recruiters_count, company: company)
end
before(:create) do |company, evaluator|
FactoryGirl.create_list(:recruiter, evaluator.recruiters_count, company: company)
end
end
end
In tests, when I do
company = create(:company)
I get validation error:
ActiveRecord::RecordInvalid:
Validation failed: Company has to have at least one recruiter
When I first build company and then save it, the test passes:
company = build(:company)
company = save
Of course, I don't want to change all my tests this way to make them work. How can I set up my factory to create associated model during creation Company model?
Your validate { check_recruiters_number } is unreasonable. Remove it.
Why? You need to have a valid company id to save recruiters, but your validator prevent company to be valid because it has no recruiters. This is in contradiction.
This is an old question but I have similar problem and I solved it with the following code (rewritten to match your case):
FactoryGirl.define do
factory :company do
association :industry
association :user
available_disclosures 15
city 'Berlin'
country 'Germany'
ignore do
recruiters_count 2
end
after(:build) do |company, evaluator|
company.recruiters = FactoryGirl.build_list(:recruiter, evaluator.recruiters_count, company: company)
end
end
end

How to create has_and_belongs_to_many associations in Factory girl

Given the following
class User < ActiveRecord::Base
has_and_belongs_to_many :companies
end
class Company < ActiveRecord::Base
has_and_belongs_to_many :users
end
how do you define factories for companies and users including the bidirectional association? Here's my attempt
Factory.define :company do |f|
f.users{ |users| [users.association :company]}
end
Factory.define :user do |f|
f.companies{ |companies| [companies.association :user]}
end
now I try
Factory :user
Perhaps unsurprisingly this results in an infinite loop as the factories recursively use each other to define themselves.
More surprisingly I haven't found a mention of how to do this anywhere, is there a pattern for defining the necessary factories or I am doing something fundamentally wrong?
Here is the solution that works for me.
FactoryGirl.define do
factory :company do
#company attributes
end
factory :user do
companies {[FactoryGirl.create(:company)]}
#user attributes
end
end
if you will need specific company you can use factory this way
company = FactoryGirl.create(:company, #{company attributes})
user = FactoryGirl.create(:user, :companies => [company])
Hope this will be helpful for somebody.
Factorygirl has since been updated and now includes callbacks to solve this problem. Take a look at http://robots.thoughtbot.com/post/254496652/aint-no-calla-back-girl for more info.
In my opinion, Just create two different factories like:
Factory.define :user, :class => User do |u|
# Just normal attributes initialization
end
Factory.define :company, :class => Company do |u|
# Just normal attributes initialization
end
When you write the test-cases for user then just write like this
Factory(:user, :companies => [Factory(:company)])
Hope it will work.
I couldn´t find an example for the above mentioned case on the provided website. (Only 1:N and polymorphic assocations, but no habtm). I had a similar case and my code looks like this:
Factory.define :user do |user|
user.name "Foo Bar"
user.after_create { |u| Factory(:company, :users => [u]) }
end
Factory.define :company do |c|
c.name "Acme"
end
What worked for me was setting the association when using the factory.
Using your example:
user = Factory(:user)
company = Factory(:company)
company.users << user
company.save!
Found this way nice and verbose:
FactoryGirl.define do
factory :foo do
name "Foo"
end
factory :bar do
name "Bar"
foos { |a| [a.association(:foo)] }
end
end
factory :company_with_users, parent: :company do
ignore do
users_count 20
end
after_create do |company, evaluator|
FactoryGirl.create_list(:user, evaluator.users_count, users: [user])
end
end
Warning: Change users: [user] to :users => [user] for ruby 1.8.x
For HABTM I used traits and callbacks.
Say you have the following models:
class Catalog < ApplicationRecord
has_and_belongs_to_many :courses
…
end
class Course < ApplicationRecord
…
end
You can define the Factory above:
FactoryBot.define do
factory :catalog do
description "Catalog description"
…
trait :with_courses do
after :create do |catalog|
courses = FactoryBot.create_list :course, 2
catalog.courses << courses
catalog.save
end
end
end
end
First of all I strongly encourage you to use has_many :through instead of habtm (more about this here), so you'll end up with something like:
Employment belongs_to :users
Employment belongs_to :companies
User has_many :employments
User has_many :companies, :through => :employments
Company has_many :employments
Company has_many :users, :through => :employments
After this you'll have has_many association on both sides and can assign to them in factory_girl in the way you did it.
Update for Rails 5:
Instead of using has_and_belongs_to_many association, you should consider: has_many :through association.
The user factory for this association looks like this:
FactoryBot.define do
factory :user do
# user attributes
factory :user_with_companies do
transient do
companies_count 10 # default number
end
after(:create) do |user, evaluator|
create_list(:companies, evaluator.companies_count, user: user)
end
end
end
end
You can create the company factory in a similar way.
Once both factories are set, you can create user_with_companies factory with companies_count option. Here you can specify how many companies the user belongs to: create(:user_with_companies, companies_count: 15)
You can find detailed explanation about factory girl associations here.
You can define new factory and use after(:create) callback to create a list of associations. Let's see how to do it in this example:
FactoryBot.define do
# user factory without associated companies
factory :user do
# user attributes
factory :user_with_companies do
transient do
companies_count 10
end
after(:create) do |user, evaluator|
create_list(:companies, evaluator.companies_count, user: user)
end
end
end
end
Attribute companies_count is a transient and available in attributes of the factory and in the callback via the evaluator. Now, you can create a user with companies with the option to specify how many companies you want:
create(:user_with_companies).companies.length # 10
create(:user_with_companies, companies_count: 15).companies.length # 15

Resources