Factory-girl create that bypasses my model validation

Ruby on-RailsRuby on-Rails-3RspecFactory BotRspec Rails

Ruby 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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNorto23View Question on Stackoverflow
Solution 1 - Ruby on-RailsBrandanView Answer on Stackoverflow
Solution 2 - Ruby on-RailsJason DenneyView Answer on Stackoverflow
Solution 3 - Ruby on-RailsTim ScottView Answer on Stackoverflow
Solution 4 - Ruby on-RailsChris HabgoodView Answer on Stackoverflow
Solution 5 - Ruby on-RailsGabe Martin-DempesyView Answer on Stackoverflow
Solution 6 - Ruby on-RailsHAZIView Answer on Stackoverflow
Solution 7 - Ruby on-RailsacaminoView Answer on Stackoverflow
Solution 8 - Ruby on-RailsJoaoHornburgView Answer on Stackoverflow
Solution 9 - Ruby on-RailsBrendaView Answer on Stackoverflow
Solution 10 - Ruby on-RailsbrcebnView Answer on Stackoverflow