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

3

6

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
Melisenda answered 3/3, 2013 at 0:10 Comment(5)
Can you post the whole thing somewhere? Because FactoryGirl.create(:stuff) in my tests goes through before :saveMalloch
@enthrops: can you confirm that the above works in your environment, and, if so, version numbers for ruby, FactoryGirl and DataMapper?Melisenda
Check out this gist about the topic. The short answer appears to be that DataMapper isn't fully supported and callbacks require some hacking.Barbera
@JimStewart: you pointed me in the right direction. The answer is nestled in the gist: "FactoryGirl calls save! on the instance", and in the world of DataMapper the save-bang method explicitly does not run the callbacks. If you want the points, post that as an answer and I'll check it (otherwise I'll answer it myself).Melisenda
Go ahead; I just googled and scanned it and left finding the real answer to you anyway. Glad it helped!Barbera
H
3

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)

Hockett answered 14/6, 2013 at 9:42 Comment(0)
M
1

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?

Melisenda answered 5/3, 2013 at 20:56 Comment(0)
G
1

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

t = build(:test_model)
t.save
Gallup answered 22/1, 2015 at 21:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.