find_or_initialize_by in FactoryGirl - ruby-on-rails

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.

Related

Factory Girl associations creates new factory model instead of associate to existing one

error message:
Failure/Error: let(:rubric_in_grenoble){ create(:rubric_in_grenoble) }
ActiveRecord::RecordInvalid:
Validation failed: Name has already been taken
So, as I suppose, the :rubric factory is tried to be created second
time (because the :rubric_in_grenoble should have association to the
same culture as :rubric_in_wroclaw). How should I change this code to
not create again same factory model, but to associate it with the
existing one?
I use Database Cleaner
I'm writing test to the model Population.
class Population < ApplicationRecord
belongs_to :province
belongs_to :culture
validates_presence_of :province, :culture
#some methods
end
It's associated to Province
class Province < ApplicationRecord
enum terrain: [:sea, :plains, :hills, :mountains]
validates :terrain, presence: true
validates :name,
presence: true,
uniqueness: true,
format: {
with: /\A[A-Za-z ]{3,30}\z/,
message: "province name has to contain letters and spaces only"
}
has_many :populations
##some methods
end
And to Culture
class Culture < ApplicationRecord
before_validation :downcase_name
validates :name, presence: true,
format: {
with: /\A[a-z]{3,20}\z/,
message: "culture name has to contain letters only"
},
length: { minimum: 3, maximum: 20 }
##many-to-many associations to different model - tested correctly
has_many :populations
end
In my test suite I use FactoryGirl and RSpec. Rails 5.0.1. I've got also simplecov installed (I'm 99.99% sure it's not interfering, but it's better to highlight this) and DatabaseCleaner works properly.
Culture and Province models are tested and everything's fine. In my FactoryGirl file for Populations there is:
FactoryGirl.define do
factory :rubric_in_wroclaw, class: "Population" do
association :province, factory: :wroclaw
association :culture, factory: :rubric
quantity 10
end
factory :javan_in_wroclaw, class: 'Population' do
association :province, factory: :grenoble
association :culture, factory: :javan
quantity 15
end
factory :rubric_in_grenoble, class: 'Population' do
association :province, factory: :grenoble
association :culture, factory: :rubric
quantity 25
end
end
And in my population_spec.rb file I've got:
require 'rails_helper'
RSpec.describe Population, type: :model do
let(:rubric_in_wroclaw){ create(:rubric_in_wroclaw) }
let(:javan_in_wroclaw){ create(:javan_in_wroclaw) }
let(:rubric_in_grenoble){ create(:rubric_in_grenoble) }
let(:pops){ [ rubric_in_wroclaw, javan_in_wroclaw, rubric_in_grenoble] }
describe '#validations' #shoulda-matchers validations - GREEN
describe '#methods' do
describe '#global_population' do
context 'without any pop' #without any fixture created
context 'with many pops' do
before { pops } <--- there is an error
it 'is equal to 50' do
expect(Population.global_population).to eq 50
end
end
end**
end
describe '#factories' #each factory is tested separately - everything's ok
end
Repeated question (because post is very long)
So, as I suppose, the :rubric factory is tried to be created second
time (because the :rubric_in_grenoble should have association to the
same culture as :rubric_in_wroclaw). How should I change this code to
not create again same factory model, but to associate it with the
existing one?
Short Answer:
Probably you are giving duplicate value to your Province.name field. The sequence should fix the problem.
Long Answer:
Let's look at the error again.
Failure/Error: let(:rubric_in_grenoble){ create(:rubric_in_grenoble) }
ActiveRecord::RecordInvalid:
Validation failed: Name has already been taken
The exception tells us we are trying to create a record with a name twice also
I can see a uniqueness validation in Province.name.
class Province < ApplicationRecord
enum terrain: [:sea, :plains, :hills, :mountains]
validates :terrain, presence: true
validates :name,
presence: true,
uniqueness: true,
format: {
with: /\A[A-Za-z ]{3,30}\z/,
message: "province name has to contain letters and spaces only"
}
has_many :populations
##some methods
end
You didn't paste the Province factory, but I expect factory is something like this:
FactoryGirl.define do
factory :my_factory, class: "Province" do
name 'blabla'
# other fields
end
end
So we have to use sequence instead of static value.
FactoryGirl.define do
factory :grenoble, class: "Province" do
sequence(:name) { |i| "BLABLA#{(i+64).chr}" }
# other fields
end
end
Assign same Province
RSpec.describe Population, type: :model do
let(:my_province){ create(:my_province) }
let(:rubric_in_wroclaw){ create(:rubric_in_wroclaw, province: :my_province) }
let(:javan_in_wroclaw){ create(:javan_in_wroclaw, province: :my_province) }
end

Factory girl : validates associated leads to undefined method valid? for nil:Nilclass

I have model Student, which has_one :account.
All input-related data is stored inside of account. Student model just plays it's role when it comes to relations (some other models belong to student).
Problem: I can't test it with factory girl.
factory :student do
end
As I can't define anything besides it.
What I get on every attempt of #student = FactoryGirl.create(:student):
undefined method `valid?' for nil:NilClass
Any fixes?
Additional code
class Account < ActiveRecord::Base
belongs_to :account_holder, :polymorphic => true
...
end
factory :account do
sequence :name do |n|
"Name#{n}"
end
sequence :surname do |n|
"Surname#{n}"
end
sequence :phone do |n|
"8911222332#{n}"
end
sequence :email do |n|
"somemail#{n}#mail.ru"
end
student
end
Source of issue
Student has:
validates_associated_extended :account
which is basically usual validate but with error extraction for parent model.
So when FactoryGirl attempts to create student, it validates account, which is nil.
I tried this:
before(:create) {|student| build(:account, :account_holder =>student )}
in student factory, while in account factory:
association :account_holder, :factory=>:student
But it still doesn't work.
factory :student do
after :build do |student|
student.account << create(:account, :account_holder => student) if student.account_holder.nil?
end
end
This allows you to have both a valid student and to specify an account if you want to force one.
I think latest versions of FactoryGirl even allow that to be written as lazy attribute syntax:
factory :student do
account { create(:account) }
end

FactoryGirl Name "can't be blank" VS "already taken"

I'm trying to run some tests on a model "Click".
# models/click_spec.rb
describe Click do
it "should have a valid constructor" do
FactoryGirl.create(:click).should be_valid
end
end
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).
But it raise this error:
Validation failed: Name has already been taken, Slug has already been taken
The problem is that it seems that it create twice the country [name:"United States", slug:"us"]
Here's the factories used.
# 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
and the model of Country:
class Country < ActiveRecord::Base
validates :name, :slug,
presence: true,
uniqueness: { case_sensitive: false }
validates :slug,
length: { is: 2 }
end
I've tried to change the association strategy to something like this:
association :country, factory: :country, strategy: :build
But it raise this error:
Validation failed: Country can't be blank
Any idea?
Thanks,
As per the shared code,
when you call FactoryGirl.create(:click),
it will go to execute factory :click where it finds association :offer, factory: :offer
which in turn calls factory: :offer where you create a country with name "United States" and slug "us" for the first time.
Again, in factory :click, it finds association :user, factory: :user which in turn calls factory: :user where you create a country again with the same name "United States" and slug "us" for the second time.
Issue #1: Validation failed: Name has already been taken, Slug has already been taken
The above error is because of the uniqueness constraint on Country model for name and slug.
Issue #2: Validation failed: Country can't be blank
When you do association :country, factory: :country, strategy: :build then strategy: :build only creates an instance of Country, it does create a record in database.
The Country can't be blank error is because you didn't create a country record in the database for user and offer. And you must be having a validation presence: true in these two models for country OR schema level check of not null.

FactoryGirl build strategy with nested associations

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

Factory Girl + Mongoid embedded documents in fixtures

Let’s say you have the following mongoid documents:
class User
include Mongoid::Document
embeds_one :name
end
class UserName
include Mongoid::Document
field :first
field :last_initial
embedded_in :user
end
How do you create a factory girl factory which initializes the embedded first name and last initial? Also how would you do it with an embeds_many relationship?
I was also looking for this one and as I was researching I've stumbled on a lot of code and did pieced them all together (I wish there were better documents though) but here's my part of the code. Address is a 1..1 relationship and Phones is a 1..n relationship to events.
factory :event do
title 'Example Event'
address { FactoryGirl.build(:address) }
phones { [FactoryGirl.build(:phone1), FactoryGirl.build(:phone2)] }
end
factory :address do
place 'foobar tower'
street 'foobar st.'
city 'foobar city'
end
factory :phone1, :class => :phone do
code '432'
number '1234567890'
end
factory :phone2, :class => :phone do
code '432'
number '0987654321'
end
(And sorry if I can't provide my links, they were kinda messed up)
Here is a solution that allows you to dynamically define the number of embedded objects:
FactoryGirl.define do
factory :profile do
name 'John Doe'
email 'john#bigcorp.com'
user
factory :profile_with_notes do
ignore do
notes_count 2
end
after(:build) do |profile, evaluator|
evaluator.notes_count.times do
profile.notes.build(FactoryGirl.attributes_for(:note))
end
end
end
end
end
This allows you to call FactoryGirl.create(:profile_with_notes) and get two embedded notes, or call FactoryGirl.create(:profile_with_notes, notes_count: 5) and get five embedded notes.

Resources