Help! I'm in a fix .. check it:
FactoryGirl.define do
factory :card do
number "1234123412341234"
exp_month 12
exp_year 2016
association :user
before(:create) do |instance|
# Start a crypto instance with this users public key and encrypt
crypt = Modules::Crypto.new(instance.user.encryption_key_id)
instance.number = crypt.encrypt("1234123412341234")
end
trait :unencrypted do
number "1234123412341234"
end
end
end
I'm trying to figure out how to:
Trigger a callback after the :user has been created, but before the :card has been created (or the Model validations will fail since the card isn't encrypted)
Make the :unencrypted trait override the callback above.
The trick mentioned in this answer and this issue is to change the create method to save without validating. Then, you can add an after(:create) that encrypts the value.
FactoryGirl.define do
factory :card do
to_create {|instance| instance.save(validate: false) }
number "1234123412341234"
exp_month 12
exp_year 2016
user
after(:create) do |instance|
# Start a crypto instance with this users public key and encrypt
crypt = Modules::Crypto.new(instance.user.encryption_key_id)
instance.number = crypt.encrypt("1234123412341234")
end
trait :unencrypted do
number "1234123412341234"
after(:create) do |instance|
# This is a noop to override previous after(:create)
end
end
end
end
Also note that "if the factory name is the same as the association name, the factory name can be left out."
Related
I’m using Rails 6.2. I have a user factory (FactoryBot 6.2) that I set up like so
FactoryBot.define do
factory :user do
…
after(:build) do |user, vars|
…
if vars.addresses.nil?
user.addresses = build_list(:address, 1, user: user)
end
end
My user model has
has_many :addresses
And my address model has
belongs_to :user
The problem with all of the above is that these calls behave the same way
create(:user, addresses: [])
And
create(:user)
In the first case, I want an empty array to be assigned to the user object. In the second case, I would like the addresses auto-created using my address factory. How do I distinguish between passing in an empty array and not passing in anything at all?
I tend to use transients for this sort of thing to make it clear what I'm trying to do.
So something like:
FactoryBot.define do
factory :user do
transient do
with_addresses { true }
end
...
after(:build) do |user, vars|
if vars.with_addresses == true
user.addresses = build_list(:address, 1, user: user)
elsif vars.with_addresses
user.addresses = vars.with_addresses
end
end
end
end
and then:
create(:user) # Builds one address for the user
create(:user, with_addresses: []) # Starts with no addresses
create(:user, with_addresses: [a1, a2]) # Starts with two addresses
And you could make it a little smarter and more flexible so that you could say things like:
create(:user, with_addresses: 0) # To create with none
create(:user, with_addresses: 6) # To get 6 from your address factory
You'd just need a couple small tweaks to the after(:build) block.
I have a FactoryGirl for a model class. In this model, I defined some traits. In some traits, I don't want FactoryGirl callback calling but I don't know how. For example here is my code:
FactoryGirl.define do
factory :product do
sequence(:promotion_item_code) { |n| "promotion_item_code#{n}" }
after :create do |product|
FactoryGirl.create_list :product_details, 1, :product => product
end
trait :special_product do
# do some thing
# and don't want to run FactoryGirl callback
end
end
In this code, I don't want :special_product trait calls after :create. I don't know how to do this.
#Edit: the reason I want to this because sometimes I want generate data from parent -> children. But sometimes I want vice versa generate from children to parent. So When I go from children -> parent, callback at parent is called so children is created twice. That is not what I want.
#Edit 2: My question is prevent callback from FactoryGirl, not from ActiveRecord model.
Thanks
You can use transient attributes to achieve that.
Like:
factory :product do
transient do
create_products true
end
sequence(:promotion_item_code) { |n| "promotion_item_code#{n}" }
after :create do |product, evaluator|
FactoryGirl.create_list(:product_details, 1, :product => product) if evaluator.create_products
end
trait :special_product do
# do some thing
# and don't want to run FactoryGirl callback
end
end
But I think that a better way to model this problem is to define a trait for the "base case" or to have multiple factories.
You could use the same approach as described in the Factory Girl docs for a has_many relationship:
factory :product_detail do
product
#... other product_detail attributes
end
factory :product do
sequence(:promotion_item_code) { |n| "promotion_item_code#{n}" }
factory :product_with_details do
transient do
details_count 1 # to match your example.
end
after(:create) do |product, evaluator|
create_list(:product_detail, evaluator.details_count, product: product)
end
end
trait :special_product do
# do some thing
# and don't want to run FactoryGirl callback
end
end
This allows you to generate data for the parent->children:
create(:product_with_details) # creates a product with one detail.
create(:product_with_details, details_count: 5) # if you want more than 1 detail.
...and for the special product just
# does not create any product_details.
create(:product)
create(:product, :special_product)
To generate for children->parent
create(:product_detail)
Assume I have the following Rails models and the method shown is tested.
class Employee < ActiveRecord::Base
has_many :jobs
def total_annual_income
jobs.collect { |j| j.annual_salary}.sum
# Or some other AR magic to do it directly in the database; doesn't matter
end
end
class Job < ActiveRecord::Base
# property :annual_salary
belongs_to :employee
end
Now, assume that I'm going to write some other method elsewhere that calls Employee#total_annual_income. When I test this method with FactoryGirl, is it possible to set up my Employee factories directly with a total_annual_income property without having to make corresponding Job factories? I.e., can I simply do
FactoryGirl.define do
factory :employee1, class: Employee do
id 100
total_annual_income 100000.0
end
end
instead of
FactoryGirl.define do
factory :employee1, class: Employee do
id 100
end
end
# WANT TO OMIT THIS ENTIRE SET OF FACTORIES #
FactoryGirl.define do
factory :employee1_job1, class: Job do
id 100
employee_id 100
annual_salary 60000.0
end
factory :employee1_job2, class: Job do
id 101
employee_id 100
annual_salary 40000.0
end
end
# WANT TO OMIT THIS ENTIRE SET OF FACTORIES #
I'm kinda new to FactoryGirl still, so apologies if I've overlooked something basic.
Have a look at the Associations information under the Factory Girl documentation:
https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md#associations
This has an example of :user_with_posts which uses #create_list to generate a list of posts for a user - a bit like your list of jobs. Since on StackOverflow it is customary to include full answers in case an external link should become broken, here's copypasta of the example with its comments:
Generating data for a has_many relationship is a bit more involved, depending on the amount of flexibility desired, but here's a surefire example of generating associated data.
FactoryGirl.define do
# post factory with a `belongs_to` association for the user
factory :post do
title "Through the Looking Glass"
user
end
# user factory without associated posts
factory :user do
name "John Doe"
# user_with_posts will create post data after the user has been created
factory :user_with_posts do
# posts_count is declared as a transient attribute and available in
# attributes on the factory, as well as the callback via the evaluator
transient do
posts_count 5
end
# the after(:create) yields two values; the user instance itself and the
# evaluator, which stores all values from the factory, including transient
# attributes; `create_list`'s second argument is the number of records
# to create and we make sure the user is associated properly to the post
after(:create) do |user, evaluator|
create_list(:post, evaluator.posts_count, user: user)
end
end
end
end
This allows us to do:
create(:user).posts.length # 0
create(:user_with_posts).posts.length # 5
create(:user_with_posts, posts_count: 15).posts.length # 15
The core of this is really that #create_list method shown above.
[EDIT] Completely untested, I think your example becomes something like:
FactoryGirl.define do
factory :employee_with_jobs, class: Employee do
id 100
transient do
jobs_count 2
end
after(:create) do |employee, evaluator|
create_list(:job, evaluator.jobs_count,
employee: employee,
annual_salary: 40000.0)
end
end
end
create(:employee_with_jobs, jobs_count: 5) # Expecting total salary 200000.0.
...more or less.
I am using Ruby on Rails 3.0.9, RSpec-rails 2 and FactoryGirl. I am trying to state a Factory association model but I am in trouble.
I have a factories/user.rb file like the following:
FactoryGirl.define do
factory :user, :class => User do
attribute_1
attribute_2
...
association :account, :factory => :users_account, :method => :build, :email => 'foo#bar.com'
end
end
and a factories/users/account.rb file like the following:
FactoryGirl.define do
factory :users_account, :class => Users::Account do
sequence(:email) {|n| "foo#{n}#bar.com" }
...
end
end
The above example works as expected in my spec files, but if in the factory :users_account statement I add the association :user code so to have
FactoryGirl.define do
factory :users_account, :class => Users::Account do
sequence(:email) {|n| "foo#{n}#bar.com" }
...
association :user
end
end
I get the following error:
Failure/Error: Unable to find matching line from backtrace
SystemStackError:
stack level too deep
How can I solve that problem so to access associated models from both sides\factories (that is, in my spec files I would like to use RoR association model methods like user.account and account.user)?
P.S.: I read the Factory Girl and has_one question and my case is very close to the case explained in the linked question. That is, I have an has_one association too (between User and Users::Account classes).
According to the docs, you can't just put both sides of the associations into the factories. You'll need to use their after callback to set an object(s) to return.
For instance, in the factories/users/account.rb file, you put something like
after(:build) do |user_account, evaluator|
user_account.user = FactoryGirl.build(:user, :account=>user_account)
end
For has_many associations, you'll need to use their *_list functions.
after(:build) do |user_account, evaluator|
user_account.users = FactoryGirl.build_list(:user, 5, :account=>user_account)
end
Note: I believe the example in the docs is a bit misleading it doesn't assign anything to the object. I believe it should be something like (note the assignment).
# the after(:create) yields two values; the user instance itself and the
# evaluator, which stores all values from the factory, including ignored
# attributes; `create_list`'s second argument is the number of records
# to create and we make sure the user is associated properly to the post
after(:create) do |user, evaluator|
user.posts = FactoryGirl.create_list(:post, evaluator.posts_count, user: user)
end
Spyle's excellent answer (still working with Rails 5.2 and RSpec 3.8) will work for most associations. I had a use case where a factory needed to use 2 different factories (or different traits) for a single has_many association (ie. for a scope type method).
What I ended up coming up with was:
# To build user with posts of category == 'Special' and category == 'Regular'
after(:create) do |user, evaluator|
array = []
array.push(FactoryBot.create_list(:post, 1, category: 'Regular')
array.push(FactoryBot.create_list(:post, 1, category: 'Special')
user.posts = array.flatten
end
This allowed the user to have 1 post of category 'Regular' and 1 post of category 'Special.'
I have 2 factories. Beta_user and Beta_invite. Basically before a Beta_user can validly save I have to create an entry of Beta_invite. Unfortunately these models don't have clean associations, but they do share an email field.
Factory.sequence :email do |n|
"email#{n}#factory.com"
end
#BetaInvite
Factory.define :beta_invite do |f|
f.email {Factory.next(:email)}
f.approved false
f.source "web"
end
#User
Factory.define :user do |f|
f.email {Factory.next(:email)}
f.password "password"
end
#User => BetaUser
Factory.define :beta_user, :parent => :user do |f|
f.after_build do |user|
if BetaInvite.find_by_email(user.email).nil?
Factory(:beta_invite, :email => user.email)
end
end
end
So in the beta beta_user factory I am trying to use the after_build call back to create the beta_invite factory.
However it seems to be acting async or something. Possibly doing the find_by_email fetch?
If I try this:
Factory(:beta_user)
Factory(:beta_user)
Factory(:beta_user)
I get a failure stating that there is no record of a beta_invite with that users email.
If instead I try:
Factory.build(:beta_user).save
Factory.build(:beta_user).save
Factory.build(:beta_user).save
I get better results. As if calling the .build method and waiting to save allows time for the beta_invite factory to be created. Instead of calling Factory.create directly. The docs say that in the case of calling Factory.create both the after_build and after_create callbacks get called.
Any help is much appreciated.
UPDATE:
So the User model I am using does a before_validation call to the method that checks if there is a beta invite. If I move this method call to before_save instead. It works correctly. Is there something i'm over looking. When does factory_girl run the after_build and after_create callbacks in relation to active-record's before_validation and before_save?
To me it seems like it just should be able to work, but I have had problems with associations in Factory-girl as well. An approach I like to use in a case like this, if the relations are less evident, is to define a special method, inside your factory as follows:
def Factory.create_beta_user
beta_invite = Factory(:beta_invite)
beta_user = Factory(:user, :email => beta_invite.email)
beta_user
end
and to use that in your tests, just write
Factory.create_beta_user
Hope this helps.
Not sure if this would help you but this is the code I used:
# Create factories with Factory Girl
FactoryGirl.define do
# Create a sequence of unique factory users
sequence(:email) { |n| "factoryusername+#{n}#example.com"}
factory :user do
email
password "factorypassword"
# Add factory user email to beta invite
after(:build) {|user| BetaInvite.create({:email => "#{user.email}"})}
end
end
I found this comment gave a really good example:
term = create(:term)
period = create(:period, term: term)
candidate = create(:candidate, term: term)
I applied it to my situation and can confirm it works.