Call trait from another trait with params in factory_girl - ruby-on-rails

I have a fairly chunky Factory Girl trait that accepts params and creates a has_many relation. I can call that trait as part of another trait to dry up traits or make it easier to bundles traits together when passing them to factories. What I don't know how to do is how to pass params to a trait when I'm calling it from another trait, or what to do instead.
e.g.
FactoryGirl.define do
factory :currency do
name Forgery::Currency.description
sequence(:short_name) { |sn| "#{Forgery::Currency.code}#{sn}" }
symbol '$'
end
factory :price do
full_price { 6000 }
discount_price { 3000 }
currency
subscription
end
sequence(:base_name) { |sn| "subscription_#{sn}" }
factory :product do
name { generate(:base_name) }
type { "reading" }
duration { 14 }
trait :reading do
type { "reading subscription" }
end
trait :maths do
type { "maths subscription" }
end
trait :six_month do
name { "six_month_" + generate(:base_name) }
duration { 183 }
end
trait :twelve_month do
name { "twelve_month_" + generate(:base_name) }
duration { 365 }
end
factory :six_month_reading, traits: [:six_month, :reading]
factory :twelve_month_reading, traits: [:twelve_month, :reading]
trait :with_price do
transient do
full_price 6000
discount_price 3000
short_name 'AUD'
end
after(:create) do |product, evaluator|
currency = Currency.find_by(short_name: evaluator.short_name) ||
create(:currency, short_name: evaluator.short_name)
create_list(
:price,
1,
product: product,
currency: currency,
full_price: evaluator.full_price,
discount_price: evaluator.discount_price
)
end
end
trait :with_aud_price do
with_price
end
trait :with_usd_price do
with_price short_name: 'USD'
end
end
end
create(:product, :with_aud_price) # works
create(:product, :with_usd_price) # fails "NoMethodError: undefined method `with_price=' for #<Subscription:0x007f9b4f3abf50>"
# What I really want to do
factory :postage, parent: :product do
with_aud_price full_price: 795
with_usd_price full_price 700
end

The :with_price trait needs to be on a separate line from the other attributes you're setting, i.e. use this:
trait :with_usd_price do
with_price
short_name: 'USD'
end
instead of this:
trait :with_usd_price do
with_price short_name: 'USD'
end

I'm on factory_bot 4.8.2 and the following is what works for me:
trait :with_usd_price do
with_price
short_name 'USD'
end

Related

Rails FactoryGirl pass empty string after create

I have a ruby app that I'm using rspec and factorygirl with, and I'm having trouble building a factory. Let me explain with an example:
I've got two factories which are creating registrant.user_demographic like below:
FactoryGirl.define do
factory :registrant do
first_name { "Johnny #{Faker::Name.initials}" }
(...)
after(:build) do |registrant|
registrant.user_demographic ||= build(:user_demographic, user: registrant)
end
end
end
FactoryGirl.define do
factory :user_demographic do
user { create(:registrant) }
phone "1234567890"
(...)
end
end
Now I want to have registrant without phone number sth like: registrant.user_demographic.phone == ''. I've tried with transient defined in user_demographic but it won't work:
FactoryGirl.define do
factory :registrant do
first_name { "Johnny #{Faker::Name.initials}" }
(...)
after(:build) do |registrant|
registrant.user_demographic ||= build(:user_demographic, user: registrant)
end
trait :without_phone do
after(:build) do |registrant|
registrant.user_demographic ||= build(:user_demographic, user: registrant, phone_present: false)
end
end
end
end
FactoryGirl.define do
factory :user_demographic do
(...)
transient do
phone_present { true }
end
phone { '1234567890' if phone_present }
end
end

question about Factory Girl syntax for passing an option to a trait

I am taking over a project that has a question / answer section. I am adding a syndication feature and would like to have a relationship where a question has_one: syndicatable_question.
For my factrory, I have an API like sq = FactoryGirl.create(:question, :with_syndication ) for the simple case and would like something like sq = FactoryGirl.create(:question, :with_syndication(syndicatable_location_id: 345)) but this doesn't work. How could I pass an option / argument for a trait? What changes would I need to make to the factory?
My factory currently looks like this:
FactoryGirl.define do
factory :question, class: Content::Question do
specialty_id 2
subject { Faker::Lorem.sentence }
body { Faker::Lorem.paragraph }
location_id 24005
trait :with_syndication do
after(:create) do |q, options|
create(:syndicatable_question, question_id: q.id, syndicatable_location_id: q.location_id)
end
end
end
end
You need to add transient block to your trait
FactoryGirl.define do
factory :question, class: Content::Question do
specialty_id 2
subject { Faker::Lorem.sentence }
body { Faker::Lorem.paragraph }
location_id 24005
transient do
syndicatable_location_id 24005
end
trait :with_syndication do
after(:create) do |q, options|
create(:syndicatable_question, question_id: q.id, syndicatable_location_id: options.syndicatable_location_id)
end
end
end
end
FactoryGirl.create(:question, :with_syndication, syndicatable_location_id: 345)
Transient Attributes
https://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits

Factorybot - How to set Nested attributes

I beleive it's better to create a new question... It follows my previous question my model product has many sizes (nested attributes)
I want to create Factories but I can't make it work...
A product is valid if it has at least one size (size_nameand quantity)
FactoryBot.define do
factory :product do
title { Faker::Artist.name}
ref { Faker::Number.number(10)}
price { Faker::Number.number(2) }
color { Faker::Color.color_name }
brand { Faker::TvShows::BreakingBad }
description { Faker::Lorem.sentence(3) }
attachments { [
File.open(File.join(Rails.root,"app/assets/images/seeds/image.jpg")),
] }
user { User.first || association(:user, admin: true)}
category { Category.first }
# SOLUTION 1
factory :size do
transient do
size_name {["S", "M", "L", "XL"].sample}
quantity { Faker::Number.number(2) }
end
end
# SOLUTION 2
after(:create) do |product|
create(:size, product: product)
end
# SOLUTION 3
initialize_with { attributes }
# Failure/Error: #product = create(:product, category_id: category.id)
# NoMethodError:
# undefined method `save!' for #<Hash:0x007ff12f0d9378>
end
end
In the controller spec
before(:each) do
sign_in FactoryBot.create(:user, admin: true)
category = create(:category)
#product = create(:product, category_id: category.id)
end
I don't know how to write the size attribute, my produt is still not valid (missing the size)
The error I get is validation failed,Product must exist...
You have to define a factory for size
FactoryBot.define do
factory :size do
size_name { ["S", "M", "L", "XL"].sample }
quantity { Faker::Number.number(2) }
end
end
and the product
FactoryBot.define do
factory :product do
association :size
title { Faker::Artist.name}
...
end
end
or add the build callback in the :product factory
after :build do |product|
product.sizes << create(:size)
end
Create a factory for sizes
FactoryBot.define do
factory :size do
size_name {["S", "M", "L", "XL"].sample}
quantity { Faker::Number.number(2) }
product
end
end
and one for products
FactoryBot.define do
factory :product do
title { Faker::Artist.name}
ref { Faker::Number.number(10)}
price { Faker::Number.number(2) }
color { Faker::Color.color_name }
brand { Faker::TvShows::BreakingBad }
description { Faker::Lorem.sentence(3) }
attachments { [
File.open(File.join(Rails.root,"app/assets/images/seeds/image.jpg")),
] }
user { User.first || association(:user, admin: true)}
category
end
end

write an Rspec test to test multiple nested_attibutes

I am trying to put together a test that will test an object with its nested_attributes.
I have a simple setup
class Animal < ActiveRecord::Base
has_many :animal_images
end
class AnimalImage < ActiveRecord::Base
belongs_to :animal
end
From the factory_girl docs you can create an associated object like so
FactoryGirl.define do
factory :animal, class: Animal do
ignore do
images_count 1
end
after(:create) do |animal, evaluator|
create_list(:animal_image, evaluator.images_count, animal: animal)
end
end
end
FactoryGirl.define do
factory :animal_image do
image { File.open("#{Rails.root}/spec/fixtures/yp2.jpg") }
end
end
so how would i construct a test to look at a custom validation method that validates the number of images..
custom method
def max_num_of_images
if image.size >= 4
errors.add(:base, "Only 3 images allowed")
end
end
But where would i use this, in Animal or AnimalImage model? Am i to assume the AnimalImage model as i have access to the image attribute?
So far i have this
it 'is invalid with 4 images' do
animal = FactoryGirl.create(:animal, images_count: 4)
animal_image = AnimalImage.create!(animal: animal, image: #how do i pass the 4 images i created earlier)
ap(animal)
ap(animal_image)
end
So ap(animal) will return
#<Animal:0x00000001c9bbd8> { :id => 52 }
and ap(animal_image) will return
#<AnimalImage:0x00000005457b30> { :id => 231, :animal_id => 52 }
What i need to do is create 4 animal_images with the same animal_id and have my validation fail it as there are more than 3 images..
Does anyone have any idea on how to go about this please?
Thank You
If i understood right, you can create array of images like:
FactoryGirl.define do
factory :animal do
name 'monkey'
trait :with_image
create_list(:animal_images, 3)
end
trait :with_5_images
create_list(:animal_images, 5)
end
end
end
FactoryGirl.define do
factory :animal_image do
image { File.open("#{Rails.root}/spec/fixtures/yp2.jpg") }
end
end
describe Animal do
subject(:animal) { build :animal, :with_images }
let(:invalid_animal) { build :animal, :with_5_images }
it { expect(subject.valid?).to be_truthy }
it { expect(invalid_aminal.valid?).to be_falsy }
end

FactoryGirl: Creating dynamic factories with parameters?

I have three factories that i want to DRY up. They look like this:
factory :sequenced_stamps_by_years, class: Stamp do
...
sequence(:day_date) { |n| n.years.ago }
end
factory :sequenced_stamps_by_months, class: Stamp do
...
sequence(:day_date) { |n| n.months.ago }
end
factory :sequenced_stamps_by_weeks, class: Stamp do
...
sequence(:day_date) { |n| n.weeks.ago }
end
How can i dry this up? I want to be able to create them something like this:
FactoryGirl.create_list(:sequenced_stamps_by_x, 4, x: "weeks") ## <- So that i can decide whether I want weeks, days, years, or months ago.
Is this possible?
If you don't favor the inheritance approach, there is an alternative using a parameter. Basically:
factory :stamps do
ignore do
interval :years # possible values => :years, :months, :weeks
end
sequence(:date_date) { |n| n.send(interval).ago }
# rest of attributes here
end
Now you can do:
FactoryGirl.create(:stamps, :interval => :months)
or
FactoryGirl.create(:stamps)
which defaults to years.
All this you can find in Factory Girl transient attributes
Factories can inherit from other factories. Therefore you can do something like:
factory :stamps do
# common attributes here
.....
factory: sequenced_stamps_by_years do
sequence(:day_date) { |n| n.years.ago }
end
factory: sequenced_stamps_by_months do
sequence(:day_date) { |n| n.months.ago }
end
factory: sequenced_stamps_by_weeks do
sequence(:day_date) { |n| n.weeks.ago }
end
end

Resources