ChatGPT解决这个技术问题 Extra ChatGPT

Factory-girl create that bypasses my model validation

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?


D
Dorian

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

See Jason Denney's answer below for a better solution.
since 1.9.1 you can do g.tap { |g| g.save(validate: false) }
D
Dorian

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

This is a much more elegant solution than the accepted one.
Keep in mind that if you did this for your general purpose factory you'd be skipping validations EVERY time you did create on that factory. It's probably best to use this technique only on a sub-factory (or in a trait).
You'll almost certainly want to put this in a trait. See the answer by Tim Scott, below.
T
Tim Scott

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)

Is there a way to apply this to all factories?
D
Dorian
foo = build(:foo).tap { |u| u.save(validate: false) }

Nice for the one-off case where you only need this on a single spec
G
Gabe Martin-Dempesy

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.


H
HAZI

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

a
acamino

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)

B
Brenda

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


J
JoaoHornburg

Depending on your scenario you could change validation to happen only on update. Example: :validates :expire_date, :presence => true, :on => [:update ]


J
Jacob Crofts

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

b
brcebn

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.


关注公众号,不定期副业成功案例分享
Follow WeChat

Success story sharing

Want to stay one step ahead of the latest teleworks?

Subscribe Now