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
Related
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
Background:
I am trying to create a FactoryBot object which is related with has_one/belongs_to
User has_one Car
Car has_one Style
Style has an attribute {style_number:"1234"}
Question
My controller references user, user has_one Car, Car has_one Style, and I need to set these values within FactoryBot.
How do I create a User, who also has a Car object, that has a Style object?
I read the documentation https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md
However, I am not understanding how they recommend doing this. Figured out, I need to nest the three objects, but confused on the syntax.
Controller
before_action :authenticate_user!
before_action :set_steps
before_action :setup_wizard
include Wicked::Wizard
def show
#user = current_user
#form_object = form_object_model_for_step(step).new(#user)
render_wizard
end
private
def set_steps
if style_is_1234
self.steps = car_steps.insert(1, :style_car)
else
self.steps = car_steps
end
end
def style_is_1234
if params.dig(:form_object, :style_number)
(params.dig(:form_object, :style_number) & ["1234"]).present?
else
(current_user.try(:car).try(:style).try(:style_number) & ["1234"]).present?
end
end
def car_steps
[:type,:wheel, :brand]
end
Rspec Test
Factory :User
FactoryBot.define do
factory :user, class: User do
first_name { "John" }
last_name { "Doe" }
email { Faker::Internet.email }
password { "somepassword" }
password_confirmation { "some password"}
end
end
Before method
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryBot.create(:user)
sign_in user
Test
User needs to be signed in and User.car.style.style_number needs to be set to "1234"
context "Requesting with second step CarStyle" do
it "should return success" do
get :show, params: { :id => 'car_style' }
expect(response.status).to eq 200
end
end
Currently this test fails because User.Car.Style.style_number is not set to "1234".
Trial 1 (https://github.com/thoughtbot/factory_bot_rails/issues/232)
FactoryBot.define do
factory :user, class: User do
first_name { "John" }
last_name { "Doe" }
email { Faker::Internet.email }
password { "somepassword" }
password_confirmation { "some password"}
car
end
end
FactoryBot.define do
factory :car, class: Car do
make { "Holden" }
model { "UTE" }
end
end
FactoryBot.define do
factory :style, class: Style do
color { "blue" }
for_car
trait :for_car do
association(:styable, factory: :car)
end
end
end
Error from trail 1
SystemStackError:
stack level too deep
Trail 2
I tried srng's recommendation
EDIT: For a polymorphic association try;
FactoryBot.define do
factory :car, class: Car do
make { "Holden" }
model { "UTE" }
association :stylable, factory: :style
end
end
and got error:
ActiveRecord::RecordInvalid: Validation failed: Stylable must exist
I think this is a rails 5 issue. https://github.com/rails/rails/issues/24518
However, I would like to keep my code with the adding the optional:true. Any way to do this?
Trail 3
FactoryBot.define do
factory :car, class: Car do
make { "Holden" }
model { "UTE" }
after(:create) do |car|
create(:style, stylable: car)
end
end
end
Tried Srng's second recommendation and although it worked for him, I got a slightly different error:
ActiveRecord::RecordInvalid:
Validation failed: User must exist
In order to create dependent Factories you have to create a factory for each model, and then just add the dependent Model name to your factory, ie.
FactoryBot.define do
factory :user, class: User do
first_name { "John" }
last_name { "Doe" }
email { Faker::Internet.email }
password { "somepassword" }
password_confirmation { "some password"}
car
end
end
FactoryBot.define do
factory :car, class: Car do
make { "Holden" }
model { "UTE" }
style
end
end
FactoryBot.define do
factory :style, class: Style do
color { "blue" }
end
end
EDIT:
Relevant code;
# Factories
FactoryBot.define do
factory :user, class: User do
first_name { "John" }
last_name { "Doe" }
email { Faker::Internet.email }
password { "somepassword" }
password_confirmation { "some password"}
after(:create) do |user|
user.car ||= create(:car, :user => user)
end
end
end
factory :style, class: Style do
style_number { "Blue" }
end
factory :car, class: Car do
name { "Holden" }
trait :style do
association :stylable, factory: :style
end
end
#models
class Car < ApplicationRecord
has_one :style, as: :styleable
end
class Style < ApplicationRecord
belongs_to :styleable, polymorphic: true
belongs_to :car
end
# Migrations - The belongs_to is the only important one
class CreateStyles < ActiveRecord::Migration[5.2]
def change
create_table :styles do |t|
t.string :style_number
t.belongs_to :stylable, polymorphic: true
t.timestamps
end
end
end
class CreateCars < ActiveRecord::Migration[5.2]
def change
create_table :cars do |t|
t.string :name
t.timestamps
end
end
end
There can be an another way of attaining this using a transient block in the Factory.
Hope the below snippet may help you to explore in new way.
Note: This is not tested.
## To Create a user in test case
# create(:user) # defaults to 1234 style number
# create(:user, car_style_number: 5678)
DEFAULT_STYLE_NUMBER = 1234
FactoryBot.define do
factory :user do
transient do
car_style_number { DEFAULT_STYLE_NUMBER }
end
first_name { "John" }
last_name { "Doe" }
email { Faker::Internet.email }
after(:create) do |user, evaluator|
user.car = create(:car, car_style_number: evaluator.car_style_number, user: user)
end
end
end
FactoryBot.define do
factory :car do
transient do
car_style_number { DEFAULT_STYLE_NUMBER }
end
make { "Holden" }
model { "UTE" }
after(:create) do |car, evaluator|
car.style = create(:style, style_number: evaluator.car_style_number, car: car)
end
end
end
FactoryBot.define do
factory :style do
style_number { DEFAULT_STYLE_NUMBER }
end
end
I've got two traits in my factory, and I want one of them to be included when I create the object, without it defaulting to one (so randomly pick the trait). Here's what I'm doing:
FactoryGirl.define do
factory :follow_up do
first_name { Faker::Name.first_name }
last_name { Faker::Name.last_name }
phone { Faker::PhoneNumber.cell_phone.gsub(/[^\d]/, '').gsub(/^1/, '2')[0..9] }
email { Faker::Internet.email }
email_preferred true
consent false
if [1, 2].sample == 1
by_referral
else
by_provider
end
trait :by_referral do
association :hospital
association :referral
source { FollowUp::REFERRAL}
end
trait :by_provider do
association :provider
source { FollowUp::PROVIDER }
end
end
end
However, it seems to be ignoring that if statement and going straight to by_provider trait. Anyone know how I'd do this?
Use an ignore block.
FactoryGirl.define do
factory :follow_up do
text "text"
author "Jim"
ignore do
if x == 1
by_referral
else
...
end
end
end
end
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
Why does:
User.stuff_to_extract = 'boo'
work in the rails c
But in rspec it fails with this:
Failure/Error: #user1.stuff_to_extract = 'XXXXXX'
NoMethodError:
undefined method `stuff_to_extract=' for #<User:0x105cd4e60>
require 'factory_girl'
Factory.define :user do |f|
f.sequence(:fname) { |n| "fname#{n}" }
f.sequence(:lname) { |n| "lname#{n}" }
f.sequence(:email) { |n| "email#{n}#google.com" }
f.password "password"
f.password_confirmation { |u| u.password }
f.invitation_code "xxxxxxx"
f.email_signature_to_extract ""
end
In the first case you are calling the method on the User class. In the second you are calling it on a User instance. To fix the second example use:
User.stuff_to_extract = 'XXXXXX'
or redefine your function to be available to the instance:
class User
def stuff_to_extract= stuff
...
end
end
instead of being available to the class:
class User
def self.stuff_to_extract= stuff
...
end
end