Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

why is before :save callback hook not getting called from FactoryGirl.create()?

This simple example uses DataMapper's before :save callback (aka hook) to increment callback_count. callback_count is initialized to 0 and should be set to 1 by the callback.

This callback is invoked when the TestObject is created via:

TestObject.create()

but the callback is skipped when created by FactoryGirl via:

FactoryGirl.create(:test_object)

Any idea why? [Note: I'm running ruby 1.9.3, factory_girl 4.2.0, data_mapper 1.2.0]

Full details follow...

The DataMapper model

# file: models/test_model.rb
class TestModel
  include DataMapper::Resource

  property :id, Serial
  property :callback_count, Integer, :default => 0

  before :save do
    self.callback_count += 1
  end
end

The FactoryGirl declaration

# file: spec/factories.rb
FactoryGirl.define do
  factory :test_model do
  end
end

The RSpec tests

# file: spec/models/test_model_spec.rb
require 'spec_helper'

describe "TestModel Model" do
  it 'calls before :save using TestModel.create' do
    test_model = TestModel.create
    test_model.callback_count.should == 1
  end
  it 'fails to call before :save using FactoryGirl.create' do
    test_model = FactoryGirl.create(:test_model)
    test_model.callback_count.should == 1
  end
end

The test results

Failures:

  1) TestModel Model fails to call before :save using FactoryGirl.create
     Failure/Error: test_model.callback_count.should == 1
       expected: 1
            got: 0 (using ==)
     # ./spec/models/test_model_spec.rb:10:in `block (2 levels) in <top (required)>'

Finished in 0.00534 seconds
2 examples, 1 failure
like image 553
fearless_fool Avatar asked Mar 03 '13 00:03

fearless_fool


3 Answers

At least for factory_girl 4.2 (don't know since which version it is supported), there is another workwaround through the use of custom methods to persist objects. As it is stated in a response to an issue about it in Github, it is just a matter of calling save instead of save!.

FactoryGirl.define do
  to_create do |instance|
    if !instance.save
      raise "Save failed for #{instance.class}"
    end
  end
end

Of course it is not ideal because it should be functional in FactoryGirl core, but I think right now it is the best solution and, at the moment, I'm not having conflicts with other tests...

The caveat is that you have to define it in each factory (but for me it wasn't an inconvenient)

like image 108
Waiting for Dev... Avatar answered Nov 12 '22 16:11

Waiting for Dev...


Solved.

@Jim Stewart pointed me to this FactoryGirl issue where it says "FactoryGirl calls save! on the instance [that it creates]". In the world of DataMapper, save! expressly does not run the callbacks -- this explains the behavior that I'm seeing. (But it doesn't explain why it works for @enthrops!)

That same link offers some workarounds specifically for DataMapper and I'll probably go with one of them. Still, it would be nice if an un-modified FactoryGirl played nice with DataMapper.

update

Here's the code suggested by Joshua Clayton of thoughtbot. I added it to my spec/factories.rb file and test_model_spec.rb now passes without error. Cool beans.

# file: factories.rb
class CreateForDataMapper
  def initialize
    @default_strategy = FactoryGirl::Strategy::Create.new
  end

  delegate :association, to: :@default_strategy

  def result(evaluation)
    evaluation.singleton_class.send :define_method, :create do |instance|
      instance.save ||
        raise(instance.errors.send(:errors).map{|attr,errors| "- #{attr}: #{errors}"    }.join("\n"))
    end

    @default_strategy.result(evaluation)
  end
end

FactoryGirl.register_strategy(:create, CreateForDataMapper)

update 2

Well. perhaps I spoke too soon. Adding the CreateForDataMapper fixes that one specific test, but appears to break others. So I'm un-answering my question for now. Someone else have a good solution?

like image 44
fearless_fool Avatar answered Nov 12 '22 15:11

fearless_fool


Use build to build your object, then call save manually...

t = build(:test_model)
t.save
like image 1
vanboom Avatar answered Nov 12 '22 14:11

vanboom