97

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?

Dave Schweisguth
  • 36,475
  • 10
  • 98
  • 121
Norto23
  • 2,249
  • 3
  • 23
  • 40

12 Answers12

109

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
Dorian
  • 7,749
  • 4
  • 38
  • 57
Brandan
  • 14,735
  • 3
  • 56
  • 71
84

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
Dorian
  • 7,749
  • 4
  • 38
  • 57
Jason Denney
  • 3,191
  • 1
  • 18
  • 14
  • 7
    This is a much more elegant solution than the accepted one. – Kyle Heironimus Mar 04 '16 at 04:19
  • 6
    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). – tgf Jul 05 '17 at 00:01
  • You'll almost certainly want to put this in a trait. See the answer by Tim Scott, below. – David Hempy Dec 12 '17 at 18:48
58

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)
Tim Scott
  • 15,106
  • 9
  • 65
  • 79
17
foo = build(:foo).tap { |u| u.save(validate: false) }
Dorian
  • 7,749
  • 4
  • 38
  • 57
Chris Habgood
  • 400
  • 3
  • 11
8

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.

Gabe Martin-Dempesy
  • 7,687
  • 4
  • 33
  • 24
6

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
HAZI
  • 69
  • 1
  • 3
2

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)
acamino
  • 29
  • 1
  • 3
1

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

Brenda
  • 273
  • 4
  • 12
0

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

JoaoHornburg
  • 917
  • 1
  • 11
  • 22
0

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
Jacob Crofts
  • 176
  • 2
  • 12
0

Another solution is to create a custom strategy:

# spec/support/factory_bot.rb

class SkipValidationStrategy
  def initialize
    @strategy = FactoryBot.strategy_by_name(:build).new
  end

  delegate :association, to: :@strategy

  def result(evaluation)
    @strategy.result(evaluation).tap do |instance|
      instance.save!(validate: false)

      raise "Instance of #{instance.class} is expected to be invalid" if instance.valid?
    end
  end
end

FactoryBot.register_strategy(:create_invalid, SkipValidationStrategy)

Then in your specs you can use

let!(:current_group) { create_invalid(:group, expiry: 1.week.since) }

See here: https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#custom-strategies

Serge
  • 83
  • 2
  • 6
-1

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.

brcebn
  • 1,571
  • 1
  • 23
  • 46