Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

FactoryGirl to_create return value

Tags:

factory-bot

From my understanding, the return value from a factory's 'to_create' method is ignored. This means that the object returned from the 'build' or 'initialize_with' portion of the factory is the object ultimately returned when calling 'create' within a test.

In my case, I am using a variant of the Repository Pattern. I am overriding the 'to_create' portion of the factory to include a call to a repository 'save' method. This method does not modify the given object, but returns an object representing the persisted form of the original.

However, the instance returned from the 'build' block is returned from the factory, and not the instance created in the 'to_create' block. In my code, this means the "unpersisted" form of the object is returned, not the object with updated attributes (e.g. 'id') from the saving action.

Is there a way of forcing the return value of 'create' to be either the result of the 'to_create' block or some value generated within that block?

class Foo
  attr_accessor :id, :name
  ...
end

class FooRepository
  def self.create(name)
    Foo.new(name) # this object is not yet persisted and has no .id
  end

  def self.save(foo)
    # this method must not guarantee that the original Foo instance
    # will always be returned
    ...
    updated_foo # this is a duplicate of the original object
  end

  ...
end

FactoryGirl.define do
  factory :foo, class: FooRepository do
    # create an example Foo
    initialize_with { FooRepository.create(name: "Example") }
    # save the Foo to the datastore, returning what may be a duplicate
    to_create {|instance| FooRepository.save(instance)}
  end
end

describe FooRepository do
  it "saves the given Foo to the datastore" do
    foo = create(:foo)
    foo.id #=> nil
    ...
  end
end
like image 557
jneander Avatar asked Oct 03 '22 02:10

jneander


1 Answers

I don't have an answer for you beyond "raise an issue", sorry.

The default to_create callback looks like this:

$ grep to_create lib/factory_girl/configuration.rb
to_create {|instance| instance.save! }

The main problem is that ActiveRecord modifies itself in place when you call save! on it. FactoryGirl will ignore any new objects that are returned from to_create.

A quick hack if you want to override the default create strategy:

module FactoryGirl
  module Strategy
    class Create
      def association(runner)
        runner.run
      end

      def result(evaluation)
        evaluation.object.tap do |instance|
          evaluation.notify(:after_build, instance)
          evaluation.notify(:before_create, instance)
          instance = evaluation.create(instance) # <-- HACK
          evaluation.notify(:after_create, instance)
        end
      end
    end
  end
end

... Or do this to your to_create hook to mimic Rails' in-place modification:

to_create do |record|
  new_record = YourRepositoryHere.new.create(record)
  record.attributes = new_record.attributes # For example
  new_record # Return the record just in case the bug is fixed
end

Best of luck. :(

like image 111
Rob Howard Avatar answered Oct 11 '22 20:10

Rob Howard