Skip to content

Instantly share code, notes, and snippets.

@mcmire
Created May 4, 2012 05:49
Show Gist options
  • Save mcmire/2592384 to your computer and use it in GitHub Desktop.
Save mcmire/2592384 to your computer and use it in GitHub Desktop.
Stupid simple fixture replacement "library" (Ruby 1.9 only)
### spec/support/factories.rb ###
require_relative 'factory'
require 'faker'
module ExampleMethods
extend Factory::Mixin
# Defines:
# - build_publisher_user
# - create_publisher_user!
# - attributes_for_publisher_user
add_factory :publisher_user, 'User' do |f|
f.email = Faker::Internet.email
f.password = SecureRandom.hex(8)
f.first_name = Faker::Name.first_name
f.last_name = Faker::Name.last_name
end
alias_factory :user, :publisher_user
end
### spec/support/factory.rb ###
# Stupid simple fixture replacement "library"
require 'ostruct'
# Factory is just the namespace for all things factory-related.
#
module Factory
@factory_classes = {}
class << self
# Public: Define a factory which will manufacture objects of a given class.
#
# mod - A Module into which the factory methods will be placed.
# name - The Symbol name of the factory.
# model_class_name - A String name of a class. The manufactured objects
# will be of this class.
# opts - A Hash of options (default: {}):
# parent - The Symbol name of an existing factory; this
# factory will inherit attributes from its
# parent factory.
#
def add_factory(mod, name, model_class_name, opts={}, &block)
name = name.to_sym
@factory_classes[name] = factory_class =
_factory_class_for(model_class_name, opts[:parent])
factory_class.defaults(&block)
mod.instance_eval do
define_method(:"build_#{name}") {|*args| factory_class.build(*args) }
define_method(:"create_#{name}!") {|*args| factory_class.create!(*args) }
define_method(:"attributes_for_#{name}") { factory_class.defaults }
end
factory_class
end
# Internal: Alias a factory by aliasing the methods for that factory.
#
# mod - A module in which the methods will be aliased.
# new_name - The Symbol name of a new factory.
# old_name - The Symbol name of an existing factory.
#
def alias_factory(mod, new_name, old_name)
mod.instance_eval do
alias_method :"build_#{new_name}", :"build_#{old_name}"
alias_method :"create_#{new_name}!", :"create_#{old_name}!"
alias_method :"attributes_for_#{new_name}", :"attributes_for_#{old_name}"
end
end
def _factory_class_for(model_class_name, parent_factory_name=nil)
parent_factory_class = parent_factory_name ? @factory_classes[parent_factory_name.to_sym] : Base
Class.new(parent_factory_class).tap do |klass|
klass.model_class_name = model_class_name
end
end
end
# Factory::Base represents a single factory.
#
class Base
@defaults = {}
class << self
# Get the name of the class this factory will use to manufacture objects.
attr_reader :model_class_name
# Set the name of the class this factory will use to manufacture objects.
attr_writer :model_class_name
# Make a new object.
#
# attrs - A Hash of attributes (default: {}). These will be merged with
# the set default attributes for this factory.
#
# Returns some Object.
#
def build(attrs={})
_model_class.new(_assemble_attrs(attrs))
end
# Make a new object and save it, if possible.
#
# attrs - A Hash of attributes (default: {}). These will be merged with
# the set default attributes for this factory.
#
# Returns some Object.
#
def create!(attrs={})
model = build(attrs)
model.save! if model.respond_to?(:save!)
return model
end
# Set or get the default attributes for the factory.
#
# block - A block which is called with an instance of Hashie::Mash.
# The block is expected to return a Hash (for a set of
# attributes), which will be stored as the default attributes for
# this factory. (Optional)
#
# Returns a Hash.
#
def defaults(&block)
if block
if block.arity == 1
# OpenStruct isn't a hash, so we can't do hashy stuff with it
attrs = Hashie::Mash.new
block.call(attrs)
@defaults = attrs
else
@defaults = block.call
end
else
@defaults
end
end
def _assemble_attrs(attrs)
@defaults.merge(attrs)
end
def _model_class
@_model_class ||= model_class_name.constantize
end
end
end
# This module should be mixed into your RSpec context, like so:
#
# RSpec.configure do |c|
# c.extend(Factory::Mixin)
# end
#
module Mixin
# Public: Define a factory which will manufacture objects of a given class.
#
# This will define three methods in the current context:
#
# build_NAME - Creates a new object from a class, populating the
# object with default attributes (and any additional
# attributes provided at runtime)
# create_NAME! - Creates a new object from a class and calls #save!
# on it (if that class has a #save! method)
# attributes_for_NAME - Return the default set of attributes defined in the
# factory.
#
# Arguments:
#
# name - The Symbol name of the factory.
# model_class_name - A String name of a class. The manufactured objects
# will be of this class.
# opts - A Hash of options (default: {}):
# parent - The Symbol name of an existing factory; this
# factory will inherit attributes from its
# parent factory.
# block - A block will will be called with a Hashie::Mash; this
# lets you define the default attributes for the factory.
#
# Example:
#
# add_factory :user, 'User' do |u|
# u.first_name = "Joe"
# u.last_name = "Blow"
# u.enabled = false
# end
#
# # within tests...
# build_user(...)
# create_user!(...)
# attributes_for_user
#
def add_factory(name, model_class_name, opts={}, &block)
Factory.add_factory(self, name, model_class_name, opts, &block)
end
# Internal: Alias a factory by aliasing the methods for that factory.
#
# new_name - The Symbol name of a new factory.
# old_name - The Symbol name of an existing factory.
#
# Example:
#
# add_factory :user, 'User', do |u|
# # ...
# end
# alias_factory :customer, :user
#
# # within tests...
# build_customer(...)
# create_customer(...)
# attributes_for_customer
#
def alias_factory(new_name, old_name)
Factory.alias_factory(self, new_name, old_name)
end
end
end
### spec/spec_helper.rb ###
require_relative 'support/factories'
RSpec.configure do |c|
c.include(ExampleMethods)
end
@yellow5
Copy link

yellow5 commented May 4, 2012

You could even tweak Factory::Base to use attr_accessor instead. Then gives you a single line that provides the same behavior, and it's more clear that both getting and setting are options.

module Factory
  class Base
     # Get and set the name of the class this factory will use to manufacture objects.
    attr_accessor :model_class_name
  end
end

@mcmire
Copy link
Author

mcmire commented May 4, 2012

Oh, I know, that's what I had originally. I was just following the TomDoc recommendation to split those out since they are two separate methods. But, I'm kind of divided on that, it might be too pedantic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment