Factory-girl create that bypasses my model validation
Ruby on-RailsRuby on-Rails-3RspecFactory BotRspec RailsRuby on-Rails Problem Overview
I am using Factory Girl to create two instances in my model/unit test for a Group. I am testing the model to check that a call to .current returns only the 'current' groups according to the expiry attribute as per below...
describe ".current" do
let!(:current_group) { FactoryGirl.create(:group, :expiry => Time.now + 1.week) }
let!(:expired_group) { FactoryGirl.create(:group, :expiry => Time.now - 3.days) }
specify { Group.current.should == [current_group] }
end
My problem is that I've got validation in the model that checks a new group's expiry is after today's date. This raises the validation failure below.
1) Group.current
Failure/Error: let!(:expired_group) { FactoryGirl.create(:group, :expiry => Time.now - 3.days) }
ActiveRecord::RecordInvalid:
Validation failed: Expiry is before todays date
Is there a way to forcefully create the Group or get around the validation when creating using Factory Girl?
Ruby on-Rails Solutions
Solution 1 - Ruby on-Rails
This isn't very specific to FactoryGirl, but you can always bypass validations when saving models via save(validate: false)
:
describe ".current" do
let!(:current_group) { FactoryGirl.create(:group) }
let!(:old_group) do
g = FactoryGirl.build(:group, expiry: Time.now - 3.days)
g.save(validate: false)
g
end
specify { Group.current.should == [current_group] }
end
Solution 2 - Ruby on-Rails
I prefer this solution from https://github.com/thoughtbot/factory_girl/issues/578.
Inside the factory:
trait :without_validations do
to_create { |instance| instance.save(validate: false) }
end
Solution 3 - Ruby on-Rails
It's a bad idea to skip validations by default in factory. Some hair will be pulled out finding that.
The nicest way, I think:
trait :skip_validate do
to_create {|instance| instance.save(validate: false)}
end
Then in your test:
create(:group, :skip_validate, expiry: Time.now + 1.week)
Solution 4 - Ruby on-Rails
foo = build(:foo).tap { |u| u.save(validate: false) }
Solution 5 - Ruby on-Rails
For this specific date-baesd validation case, you could also use the timecop gem to temporarily alter time to simulate the old record being created in the past.
Solution 6 - Ruby on-Rails
It is not best to skip all validation of that model.
create spec/factories/traits.rb
file.
FactoryBot.define do
trait :skip_validate do
to_create { |instance| instance.save(validate: false) }
end
end
fix spec
describe ".current" do
let!(:current_group) { FactoryGirl.create(:group, :skip_validate, :expiry => Time.now + 1.week) }
let!(:expired_group) { FactoryGirl.create(:group, :skip_validate, :expiry => Time.now - 3.days) }
specify { Group.current.should == [current_group] }
end
Solution 7 - Ruby on-Rails
Your factories should create valid objects by default. I found that transient attributes can be used to add conditional logic like this:
transient do
skip_validations false
end
before :create do |instance, evaluator|
instance.save(validate: false) if evaluator.skip_validations
end
In your test:
create(:group, skip_validations: true)
Solution 8 - Ruby on-Rails
Depending on your scenario you could change validation to happen only on update. Example: :validates :expire_date, :presence => true, :on => [:update ]
Solution 9 - Ruby on-Rails
I added an attr_accessor to my model to skip the date check:
attr_accessor :skip_date_check
Then, in the validation, it will skip if so specified:
def check_date_range
unless skip_date_check
... perform check ...
end
end
Then in my factory, I added an option to create an old event:
FactoryBot.define do
factory :event do
[...whatever...]
factory :old_event do
skip_date_check { true }
end
end
end
Solution 10 - Ruby on-Rails
Or you can use both FactoryBot
and Timecop
with something like:
trait :expired do
transient do
travel_backward_to { 2.days.ago }
end
before(:create) do |_instance, evaluator|
Timecop.travel(evaluator.travel_backward_to)
end
after(:create) do
Timecop.return
end
end
let!(:expired_group) { FactoryGirl.create(:group, :expired, travel_backward_to: 5.days.ago, expiry: Time.now - 3.days) }
Edit: Do not update this event after creation or validations will fail.