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?
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
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
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)
foo = build(:foo).tap { |u| u.save(validate: false) }
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.
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
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)
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
Depending on your scenario you could change validation to happen only on update. Example: :validates :expire_date, :presence => true, :on => [:update ]
Adding a FactoryBot trait
to skip validations optionally, as some contending answers suggest, makes sense. An alternative is to stub the model for the specific test case(s) where you don't want validation. This adds a couple of lines of code but is arguably more discoverable. You also have more control over which methods to avoid calling.
Modern RSpec example:
before(:each) do
allow_any_instance_of(MyModel).
to receive(:my_validation_method).
and_return(nil)
end
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.
Success story sharing
g.tap { |g| g.save(validate: false) }