How split factory_girl definitions across many files? - ruby-on-rails

My factories.rb file became too big to maintain over time and I'm now trying to split it across many files in factories directory. The problem is that I don't know how to deal with dependencies.
To make a long story short, I'm trying to split my factories in following way. All sequences go to sequences.rb file and each factory definition goes to separate file like so:
factories/sequences.rb
FactoryGirl.define do
sequence :name {|n| "Name #{n}" }
sequence :email {|n| "person#{n}#example.com" }
end
factories/user.rb
FactoryGirl.define do
factory :user do
name
email
end
end
factories/post.rb
FactoryGirl.define do
factory :post do
name
content "Post Content"
user
end
end
When I run tests I get name is not defined error. I can deal with this by passing a block to each association (e.g. name, email, user and so on) mention but it seems to be ugly and not DRY.
Is there way to let factory_girl know sequence in which files should be loaded?
to deal with complex dependencies when it's not possible to fix this issue with changing load sequence of files?

You can simply achieve the result with generate method:
# factories/sequences.rb
FactoryGirl.define do
sequence(:email) { |n| "person#{n}#example.com" }
end
# factories/user.rb
FactoryGirl.define do
factory :user do
email { generate(:email) }
password '12345678'
end
end
Then try it:
FactoryGirl.create :user
=> #<User:0x007fa99d2ace40
id: 1,
email: "person1#example.com",
. . .>
Sequences Documentation for more details.

I do this in this way:
Create separate folder for shared factories. It should in same level as 'factories' folder.
factories
shared_factories
Create shared file, ex. shared_factories/sequences.rb
Import sequences.rb to every factory file.
require_relative '../shared_factories/sequences'
The full example:
https://gist.github.com/alexkojin/6a2d70f84ff91c37315d1d3edb0d8e6b

You can do this:
FactoryGirl.define do
factory :user do
sequence(:name) {|n| "Name #{n}" }
sequence(:email) {|n| "person#{n}#example.com" }
end
end
FactoryGirl.define do
factory :post do
content "Post Content"
user { create :user }
end
end
There are a variety of ways to structure your factory girls factories such as traits, inheritance, callbacks, etc. I just recently learned about traits and it is really wonderful:
FactoryGirl.define do
factory :user do
sequence(:name) {|n| "Name #{n}" }
sequence(:email) {|n| "person#{n}#example.com" }
end
trait :with_post do
after(:create) { |user| create :post, user: user }
end
end
FactoryGirl.define do
factory :post do
content "Post Content"
user { create :user }
end
end
# and in your specs you will do this:
let!(:blogger) { create :user, :with_post }
It is very worthwhile to invest some time reading through the entire documentation (a 30 mins read perhaps?). Thereafter, you will have much better ideas to refactor your factories.
Reference link: https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md

Related

Factorygirl set has_many as nil

I have a user factory that be default creates an association for user_document
FactoryGirl.define do
factory :user do |u|
sequence(:first_name) {|n| "firstname-#{n}" }
after(:create) do |user|
create(:user_document, document_type: :address_proof, user: user)
end
end
end
I'd like to define a new factory for which the association user_document is null. How do I do this ?
[UPDATE]
When I do this
factory :user_with_no_doc_buyer do
user_document nil
end
I get an error saying :
NoMethodError: undefined method `user_document=' for #<User:0x007f97329c08f8>
and When I do
user_documents nil I get
NoMethodError: undefined method `each' for nil:NilClass
thanks
I'd create two traits.
FactoryGirl.define do
factory :user do
sequence(:first_name) {|n| "firstname-#{n}" }
trait :with_document do
after(:create) do |user|
create(:user_document, document_type: :address_proof, user: user)
end
end
trait :without_document do
user_documents []
end
end
end
Then you just call whichever factory you need.
FactoryGirl.create(:user, :with_document) for example
Edit: Have seen you want the with document to be the default. That's easy enough. You can just use the code you have and take my suggestion of a trait without document which you can call when you need it.
Use traits
FactoryGirl.define do
factory :user do |u|
sequence(:first_name) {|n| "firstname-#{n}" }
trait :with_user_document
after(:create) do |user|
create(:user_document, document_type: :address_proof, user: user)
end
end
end
end
To create user with document
FactoryGirl.create(:user, :with_user_document)
To create user with null document
FactoryGirl.create(:user)

FactoryGirl failed validation while creating with a trait that contains required attributes

I have a User model with two roles: member and admin and defaults to member. This model has two required attributes payout_bank and payout_account and they are only required to be present if the role is member.
My FactoryGirl for User has two traits that corresponds to the roles. I then add the two required attributes to the member trait, but when I create an User with member trait with linting turned on FactoryGirl will complain that the required attributes are failing validation because they're blank.
If I turn off linting it will correctly generate a member User with payout_bank and payout_account filled out. What's going on?
Model User.rb
class User < ActiveRecord::Base
validates_presence_of :payout_bank, :payout_account, if: :member?
def set_default_role
self.role ||= :member
end
end
Factory User.rb
FactoryGirl.define do
factory :user do
sequence(:email) { |n| "user#{n}#email.com" }
password "password"
trait :member do
sequence(:email) { |n| "member#{n}#email.com" }
role "member"
payout_bank { Faker::Company.name }
payout_account { Faker::Number.number(10) }
end
trait :admin do
sequence(:email) { |n| "admin#{n}#email.com" }
role "admin"
status "active"
end
end
end
Creating user from Factory
FactoryGirl.lint
FactoryGirl.create(:member_user, :member)
Output
FactoryGirl::InvalidFactoryError: The following factories are invalid:
* member_user - Validation Failed: payout_bank cannot be blank, payout_account cannot be blank (ActiveRecord::RecordInvalid)/usr/local/lib/ruby/gems/2.3.0/gems/factory_girl-4.7.0/lib/factory_girl/linter.rb:12:in `lint!'
You have to write the validation tests in spec/models directrory. Something like.
require 'rails_helper'
require "validates_email_format_of/rspec_matcher"
RSpec.describe BankAccount, type: :model do
it "should have a account number" do
bank_a = build(:bank_account, account_no: '')
expect(bank_a).to_not be_valid
end
end
I found out that FactoryGirl.lint is the line that blew up and caught the validation failure. The issue here is that linting will try to create generic User model with no traits. Since my User model defaults to member role, and this role requires payout_bank and payout_account to be present, the validation thus failed.
The solution is to move these two attributes out of the member trait and into the base in the factory.
Something like this
FactoryGirl.define do
factory :user do
sequence(:email) { |n| "user#{n}#email.com" }
password "password"
payout_bank { Faker::Company.name } # Move here
payout_account { Faker::Number.number(10) } # Move here
trait :member do
sequence(:email) { |n| "member#{n}#email.com" }
role "member"
end
trait :admin do
sequence(:email) { |n| "admin#{n}#email.com" }
role "admin"
status "active"
payout_bank nil
payout_account nil
end
end
end

Rails: access test fixtures from factories?

Is there a way I can access test fixtures from within my factories? I'm trying to do something like:
Factory.define do
factory :foo do
user { users(:active) }
end
end
You can access fixtures in your factories using the ActiveRecord::FixtureSet class, which provides access to the fixtures defined in your test/fixtures directory. You could access the :active user fixture like this:
FactoryBot.define do
factory :foo do
user { ActiveRecord::FixtureSet.cached_fixtures[:active, 'users'] }
end
end
IMO the use of fixtures in factories is discouraged, as it couples your factories to your tests and makes it difficult to reuse your factories in other contexts. A better approach would be to create a factory for the User model with an active trait and use that within your :foo factory
FactoryBot.define do
factory :user do
trait :active do
active { true }
end
end
factory :foo do
user { create(:user, :active) }
end
end

Specifying associations to inherited factory with FactoryGirl

I've just started using FactoryGirl for my tests, and I've run into some issues.
I have user, qualification and user_qualification models. For qualifications, I have two inherited factories, namely qualification_ba and qualification_phd. Now, I'm trying to generate a qualification that includes either qualification_ba or qualification_phd factories by running:
create(:user_qualification_ba_bad_marks).
This returns:
NoMethodError: undefined method 'qualification_ba='.
However, when running:
create(:qualification_ba)
it successfully creates the object as expected. Is there perhaps a limit on how deep you can nest factories?
factory :user do
to_create {|instance| instance.save(validate: false) }
full_name Faker::Name.name
email Faker::Internet.email
password Faker::Internet.password(8)
factory :user_ba_candidate do
before(:create) do |user|
create_list(:user_qualification_ba_bad_marks, 1, user: user)
end
end
factory :user_phd_candidate do
before(:create) do |user|
create_list(:user_qualification_phd_good_marks, 1, user: user )
end
end
end
factory :qualification do
user_selectable true
factory :qualification_ba do
name "Bachelors"
qualification_level 2
end
factory :qualification_phd do
name "Masters"
qualification_level 5
end
end
factory :user_qualification do
to_create {|instance| instance.save(validate: false) }
factory :user_qualification_ba_bad_marks do
qualification_ba
aggregate (30..50).to_a.sample
end
factory :user_qualification_phd_good_marks do
qualification_phd
aggregate (70..90).to_a.sample
end
user
end

Factories assign association after created

I have a question in Rails 4.
On my factories.rb i have something like:
factory :user do
sequence(:name) { |n| "User #{n}" }
sequence(:email) { |n| "user_#{n}#example.org" }
password 'foobar'
password_confirmation 'foobar'
end
The problem, i don't know to add a relation after created user.
When i create a user, and he is a moderator i assign to user the role like that:
u.add_role_id!(11)
I have different table for relationships, with user_id and role_id and on user model i have the method.
def add_role_id!(role)
role_user_relationship.create!(role_id: Role.find(role).id)
end
How i do that on factories.rb?
Now what i now to do:
u = FactoryGirl.create(:moderator)
u.add_role_id!(11)
I want to assign by default on factories.rb
How I do that?!
Thanks everybody!
Manu
Solution:
I have to add these lines:
after(:create) do |u|
u.add_roles_ids!([10,20,30])
end
With the entire code:
factory :user do
sequence(:name) { |n| "User #{n}" }
sequence(:email) { |n| "user_#{n}#mudev.org" }
password 'foobar'
password_confirmation 'foobar'
after(:create) do |u|
u.add_roles_ids!([10,20,30])
end
end
Thanks to Peter Alfvin
You can use FactoryGirl's after callback mechanism described at https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md#callbacks to get the comparable behavior to what you're doing now by including the following within your factory definition:
after(:create) {|u| u.add_role_id!(11)
Depending on your validation, you can alternatively do this after(:build) or before(:create).

Resources