Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support alternative ORMs (DataMapper) #372

Closed
Ragmaanir opened this issue May 5, 2012 · 5 comments
Closed

Support alternative ORMs (DataMapper) #372

Ragmaanir opened this issue May 5, 2012 · 5 comments

Comments

@Ragmaanir
Copy link

Hi,

i like FactoryGirl and i am using it together with DataMapper. But making FG work with DM is sometimes a bit messy (monkey patching) and time consuming (e.g. when upgrading FG). My last monkey-patches looked like this:

class FactoryGirl
    class Proxy
        class Create < Build
            def result
                run_callbacks(:after_build)
                #@instance.save or raise(@instance.errors.inspect)
                @instance.save or raise(@instance.errors.send(:errors).map{|attr,errors| "- #{attr}: #{errors}" }.join("\n"))
                run_callbacks(:after_create)
                @instance
            end
        end
    end
end

#2.2.0
module FactoryGirl
    class Proxy #:nodoc:
        class Create < Build #:nodoc:
            def result(to_create)
                run_callbacks(:after_build)
                #raise unless to_create
                #to_create.call(@instance)
                @instance.save or raise(@instance.errors.send(:errors).map{|attr,errors| "- #{attr}: #{errors}" }.join("\n"))
                run_callbacks(:after_create)
                @instance
            end
        end
    end
end

# FactoryGirl 2.5.1
module FactoryGirl
  class Proxy #:nodoc:
    class Create < Build #:nodoc:
      def result(attribute_assigner, to_create)
        attribute_assigner.object.tap do |result_instance|
          run_callbacks(:after_build, result_instance)
          #to_create[result_instance]
          result_instance.save or raise(result_instance.errors.send(:errors).map{|attr,errors| "- #{attr}: #{errors}" }.join("\n"))
          run_callbacks(:after_create, result_instance)
        end
      end
    end
  end
end

# FactoryGirl 2.6.0
module FactoryGirl
  class Strategy
    class Create < Strategy
      def result(attribute_assigner, to_create)
        attribute_assigner.object.tap do |result_instance|
          run_callbacks(:after_build, result_instance)
          #to_create[result_instance]
          result_instance.save or raise(result_instance.errors.send(:errors).map{|attr,errors| "- #{attr}: #{errors}" }.join("\n"))
          run_callbacks(:after_create, result_instance)
        end
      end
    end
  end
end

# FactoryGirl 3.2.0
module FactoryGirl
  class Definition
    def initialize(name = nil, base_traits = [])
      @declarations      = DeclarationList.new(name)
      @callbacks         = []
      @defined_traits    = []
      @to_create         = ->(instance) {
                instance.save or raise(instance.errors.send(:errors).map{|attr,errors| "- #{attr}: #{errors}" }.join("\n"))
            }
      @base_traits       = base_traits
      @additional_traits = []
      @constructor       = nil
    end
  end
end

It would be nice to have a configuration option for the default create-method (or is there already a solution hidden somewhere? could not find one yet). Thanks :)

@joshuaclayton
Copy link
Contributor

There's currently no way to override it for all factories at once (yet), but you can override to_create on a factory to change its save behavior. Check out https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md#custom-methods-to-persist-objects to see an example of how to use it.

@joshuaclayton
Copy link
Contributor

In the meantime, instead of defining to_create on all your factories, you could override the create strategy altogether (since you're on 3.2.0):

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)

I'm not 100% sure that this code would work, but I'm guessing it will.

So, what's it doing?

I'm overriding FactoryGirl's create strategy with a new class that follows 'Composition Over Inheritance' to piggy-back off of FactoryGirl::Strategy::Create's behavior. In result, we redefine the create method (so this is some flavor of decoration, it seems) to swap out its default behavior with the custom behavior. I then pass the evaluation (with the modified create method) to result on the default strategy.

This allows for the behavior of FactoryGirl's default create strategy to change (in case we add any more callbacks, etc.) without impeding your use of those features.

The downside to this is that it'll effectively disable to_create and skip_create on all of your factories (since we're overriding the evaluation's create, which uses to_create behind the scenes).

So, the final answer is:

  1. If you have a lot of factories and want to override the to_create behavior on each, you may want to try overriding the create strategy.
  2. If you only have a few factories, define to_create on each with the behavior above.
  3. Extend DataMapper (not sure where you'd add the hook) so all of your objects using it define a save! instance method; this would require no extending of FactoryGirl and allow for removal of all hacks, since FactoryGirl calls save! on the instance already.
  4. Wait until FactoryGirl supports to_create for all factories, and then define the save behavior there.

Make sense? There's a lot of options here, and all of them will hopefully reduce the amount of strain on you and get rid of those nasty monkey-patches. Let me know if you have any questions!

@Ragmaanir
Copy link
Author

Thanks for the detailed explanation. (1) is my preferred solution, (2) is not practical since i have ~30 factories and still adding more. (3) is something i have thought about too (But in DM #save! saves the record without executing callbacks and validations).

@rdpoor
Copy link

rdpoor commented Mar 6, 2013

I can confirm that Joshua's CreateForDataMapper code given above appears to work. Cool beans.

@reagent
Copy link

reagent commented Mar 11, 2013

This is now supported in the current (as of this writing) version of FactoryGirl by using a custom persistence method globally. An example of what would work here for DataMapper specifically:

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

  factory :user do
    sequence(:email)      {|n| "user_#{n}@host.com" }
    password              'sekrit'
    password_confirmation 'sekrit'
  end
end

Hope that helps someone who lands here from a Google search.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants