FactoryGirl build strategy with nested associations - ruby-on-rails

Is it possible to preserve the build strategy when I have a factory for a model that has an association to a second model, which itself has an association to a third model?
In the example below, a Post is associated with a User, and a User is associated with a City. Even when :strategy => :build is used for all associations, post.user and post.user.city end up getting saved to the database. In the interests of a speedy test suite, can I prevent these database writes from happening?
Factory.define do
factory :user do
name "A User"
association :city, :strategy => :build
end
factory :city do
name "A City"
end
factory :post do
title "A Post"
body "Some text here"
association :user, :strategy => :build
end
end
post = FactoryGirl.build(:post)
post.new_record? # True
post.user.new_record? # False
post.user.city.new_record? # False

Have you tried the alternative block syntax?
Factory.define do
factory :user do
name "A User"
city { |city| city.association :city, :strategy => :build }
end
factory :city do
name "A City"
end
end

It looks like FactoryBot (formerly FactoryGirl) added use_parent_strategy as a configuration option in v4.8.0. It is turned off by default, to turn it on add the following to your spec/rails_helper:
FactoryGirl.use_parent_strategy = true
Relevant pull request on the factory_bot repo: https://github.com/thoughtbot/factory_bot/pull/961

As #messanjah said, however for older versions (< v4.8.0) you can do the following:
association :user, :strategy => #build_strategy.class

Related

FactoryGirls multiple associations

I'm having the hardest time thinking through this. I'm very new to FactoryGirl, so this may be explained clearly somewhere and I apologize if that is the case. This is certainly not a unique problem, maybe my Google skills aren't quite up to par.
I am working orders, which belong to category and belong to customer. I'm trying to build a customer who's placed 5 orders, however I keep throwing unique errors when it tries to build the category (which requires a unique name).
features/customer_spec.rb
RSpec.feature "Customer management", :type => :feature do
scenario "Customer with orders has order history" do
customer = create(:customer, :with_5_completed_orders)
visit customer_path(customer)
expect(page).to have_content("Recent Orders")
end
end
factories/customers.rb
FactoryGirl.define do
factory :customer do
...
trait :with_5_completed_orders do
after :create do |customer|
create_list(:order_line, 5, :completed, :customer => customer)
end
end
end
end
factories/order_line.rb
FactoryGirl.define do
factory :order_line do
....
product
....
end
end
factories/product.rb
FactoryGirl.define do
factory :product do |f|
....
category
....
end
end
factories/categories.rb
FactoryGirl.define do
sequence :category_name do |n|
"category-#{n}"
end
factory :category do
name { generate(:category_name) }
end
end
If I get it right, you have problem with category name uniqueness.
You can use sequences to generate unique category names with FactoryGirl:
sequence :category_name do |n|
"category-#{n}"
end
factory :category do
name { generate(:category_name) }
end
FactoryGirl's documentation on sequences is worth reading too.
If your intention is to reuse the same category twice, you can first create one and reuse it in all orders:
trait :with_5_completed_orders do
after :create do |customer|
cat = generate(:category)
create_list(:order_line, 5, :completed, customer: customer, category: cat)
end
end

find_or_initialize_by in FactoryGirl

I was wondering if there's an equivalent for find_or_initialize_by in FactoryGirl that solve teh following issue:
The objective is that the model uses two tables that have the same country. I don't want to use a sequence for the country (as I found for Emails).
There's a uniqueness constraint on Country, but my main issue is that it create twice the same record of Country when I call once FactoryGirl.create(:click)
Thus, the Validation fail in the test.
Rspec:
# models/click_spec.rb
describe Click do
it "should have a valid constructor" do
FactoryGirl.create(:click).should be_valid
end
end
Factories:
# factories/countries.rb
FactoryGirl.define do
factory :country do
name "United States"
slug "us"
end
end
# factories/offers.rb
FactoryGirl.define do
factory :offer do
association :country, factory: :country
# Other columns
end
end
# factories/users.rb
FactoryGirl.define do
factory :user do
association :country, factory: :country
# Other columns
end
end
# factories/clicks.rb
FactoryGirl.define do
factory :click do
association :offer, factory: :offer
association :user, factory: :user
# Other columns
end
end
Model:
class Country < ActiveRecord::Base
validates :name, :slug,
presence: true,
uniqueness: { case_sensitive: false }
validates :slug,
length: { is: 2 }
end
You should be able to make this work by using initialize_with:
FactoryGirl.define do
factory :country do
name "United States"
slug "us"
initialize_with { Country.find_or_create_by_name(name) }
end
end
This will always use the same country. You may want to nest the factory to allow other factories to use different names:
FactoryGirl.define do
factory :country do
initialize_with { Country.find_or_create_by_name(name) }
factory :united_states do
name "United States"
slug "us"
end
end
end
I faced similar issues, also with the Country model of my application. Here's what I did.
To ensure FactoryBot's build and create still behaves as it should, we should only override the logic of to_create, by doing:
factory :country do
to_create do |instance|
instance.id = Country.create_with(name: instance.name).find_or_create_by(slug: instance.slug).id
instance.reload
end
name { "United States" }
slug { "us" }
end
Query explained:
Country
.create_with(name: instance.name) # if not found, create with this `name` (and `slug` defined below)
.find_or_create_by(slug: instance.slug) # find by primary key `slug'
This ensures build maintains it's default behavior of "building/initializing the object" and does not perform any database read or write so it's always fast. Only logic of create is overridden to fetch an existing record if exists, instead of attempting to always create a new record.
Originally posted on https://stackoverflow.com/a/55235861/3956879.
Check out my article explaining this.

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

FactoryGirl define attribute by calling method on another factory

Here is an example from the FactoryGirl documentation:
FactoryGirl.define do
factory :post do
name "Post name"
user
end
end
In this example, user is invoking another factory. What I would like to do is effectively call user.id, but to set it as the definition of an attribute. Here's a stripped-down example:
**models/job.rb**
...
belongs_to :assignee, :class_name => "User"
belongs_to :user
...
attr_accessible :assignee_id, :user_id
...
end
**factories/jobs.rb**
FactoryGirl.define do
factory :job do
assignee_id user.id #what I would like to do, but triggers "undefined method 'id'" error
user_id user.id #user_id is an attribute of the model and is the job assignor
end
I've tried to incorporate the part of the documentation that discusses aliases, but with no luck:
FactoryGirl.define do
factory :user, :aliases => [:assignee] do
....
I feel like (hope?) I'm close here, but any insight is appreciated. Thanks.
EDIT: This code gets my specs running!
**factories/jobs.rb**
FactoryGirl.define do
factory :job do
before(:create) do |job|
user = FactoryGirl.create(:user)
job.assignee = user
job.user = user
end
association :assignee, factory: :user
association :user, factory: :user
sequence(:user_id) { |n| n }
sequence(:assignee_id) { |n| n }
...
end
And it passes my it { should be_valid } spec, so it seems that the factory is fine, though I think I have some refactoring in the spec itself when I'm calling FactoryGirl.create.
The code above incorporates the suggestions from mguymon. Thanks!
FINAL UPDATE
After going back and re-reading Hartl's discussion on model associations, I was able to put this matter to rest. What I have above was techincally valid, but didn't actually pass the attributes in properly when i built or created jobs in my spec. Here's what I should have had:
FactoryGirl.define do
factory :job do
association :assignee, factory: :user
user
end
end
My problem also stemmed from how I was creating factories in my spec, so here's how I should have been doing it (but wasn't...sigh):
let(:user) { create(:user) }
before { #job = create(:job, user: #user) }
It seems that I don't explicitly have to have association :user in my factory, nor do I need the before block from above.
As an aside, I also learned that I can debug by including puts #job within an expect statement, or call #job.assignee_id to make sure that the attributes are being loaded properly. When that particular spec is run, the puts statement will output right by the F or . from the spec.
For the latest version of FactoryGirl, use association to map to other ActiveRecord Models:
factory :job do
# ...
association :assignee, factory: :user
end
This is straight from the docs.
From the error you posted, it is stating you are trying to get the user.id but user is not an ActiveRecord instance but a proxy from FactoryGirl. You will not get this error if you are using the association method. If you need to directly access a model, you have to manually build it first. You can do this by passing a block in your factory:
factory :job do
assignee_id { FactoryGirl.create(:user).id }
end
It looks like you are trying to associate the same model twice, for this you can use before_create callback to create a User model and assign to user and assignee:
factory :job do
before(:create) do |job|
user = FactoryGirl.create(:user)
job.assignee = user
job.user = user
end
end

Extra arguments for Factory Girl

I need to pass extra arguments to factory girl to be used in a callback. Something like this (but more complex really):
Factory.define :blog do |blog|
blog.name "Blah"
blog.after_create do |blog|
blog.posts += sample_posts
blog.save!
end
end
and then create it with something like this:
Factory.create(:blog, :sample_posts => [post1, post2])
Any ideas how to do it?
This is now possible without any "hacks" thanks to transient attributes (see comment on issue #49)
example:
FactoryGirl.define do
factory :user do
transient do
bar_extension false
end
name {"foo #{' bar' if bar_extension}"}
end
end
# Factory(:user).name = "foo"
# Factory(:user, :bar_extension => true).name = "foo bar"
For Factory Girl versions < 5.0:
FactoryGirl.define do
factory :user do
ignore do
bar_extension false
end
name {"foo #{' bar' if bar_extension}"}
end
end
# Factory(:user).name = "foo"
# Factory(:user, :bar_extension => true).name = "foo bar"
One option would be to create a virtual accessor for extra posts that the after_create hook checks:
class Blog
has_many :posts
attr_accessible :name, :title, ... # DB columns
attr_accessor :sample_posts # virtual column
end
Factory.define :blog do |blog|
blog.name 'Blah'
blog.after_create do |b|
b.posts += b.sample_posts
b.save!
end
end
Factory(:blog, :sample_posts => [post1, post2])
Apparently, this is not possible at the moment without workarounds that require modifying the model itself. This bug is reported in: http://github.com/thoughtbot/factory_girl/issues#issue/49
Another option would be to use build instead of create and add :autosave to the collection:
class Blog
has_many :posts, :autosave => true
end
Factory.define :blog do |blog|
blog.name 'Blah'
blog.posts { |_| [Factory.build(:post)] }
end
Factory(:blog, :posts => [post1, post2])
#or
Factory.build(:blog, :posts => [unsavedPost1, unsavedPost2])
if you are opening the class inside the factorygirl file, i suggest doing it like
require "user"
class User
attr :post_count
end
so that you are opening the class, instead of overwriting it

Resources