Permalink
Browse files

First implementation

  • Loading branch information...
1 parent 77dda0d commit b18c0cb65bd4f55b410937ed8c69e9fceb4b9148 @patshaughnessy committed Mar 10, 2010
Showing with 442 additions and 23 deletions.
  1. +57 −13 README.rdoc
  2. +3 −3 Rakefile
  3. +1 −0 VERSION
  4. +57 −0 class_factory.gemspec
  5. +7 −0 lib/class_factory.rb
  6. +63 −0 lib/class_factory/class_factory.rb
  7. +3 −0 test/helper.rb
  8. +124 −0 test/test_active_records.rb
  9. +0 −7 test/test_class_factory.rb
  10. +127 −0 test/test_plain_objects.rb
View
70 README.rdoc
@@ -1,17 +1,61 @@
-= class_factory
+= Class Factory: Factory_girl syntax for dynamically creating Ruby classes
-Description goes here.
+Class Factory will dynamically create classes using a factories model similar to {factory_girl}[http://github.com/thoughtbot/factory_girl]. But instead of passing a block with model attributes into the factory definition, you pass in a migration defining the attributes of the new model class you want to create:
-== Note on Patches/Pull Requests
-
-* Fork the project.
-* Make your feature addition or bug fix.
-* Add tests for it. This is important so I don't break it in a
- future version unintentionally.
-* Commit, do not mess with rakefile, version, or history.
- (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
-* Send me a pull request. Bonus points for topic branches.
+ ClassFactory.define :person do |p|
+ p.string :first_name
+ p.string :last_name
+ p.integer :age
+ end
-== Copyright
+Now when you need a "Person" model in your tests you create one like this:
-Copyright (c) 2010 Pat Shaughnessy. See LICENSE for details.
+ ClassFactory :person
+ => Person(id: integer, first_name: string, last_name: string, age: integer)
+
+This can be useful if you're writing tests for a gem or plugin and don't want to load the entire Rails environment, or have access to existing models in a target application. By default Class Factory creates ActiveRecord model classes, but using the :super option you can create any sort of Ruby class. Class Factory also makes it easy for each of your tests to use a different variation on a target class. For example, this will delete the Person model we created above, and create a new Person model that belongs to a group:
+
+ ClassFactory :person, :class_eval => 'belongs_to :group' do |p|
+ p.string :first_name
+ p.string :middle_name
+ p.string :last_name
+ p.string :group_id
+ end
+ => Person(id: integer, first_name: string, middle_name: string, last_name: string, group_id: string)
+
+Creating different variations of the same class can be useful if you're writing tests for a generator, plugin or some other code which has different behavior depending on what classes you run it against.
+
+== Install
+
+ gem install class_factory
+
+== Options
+
+Default: create a new ActiveRecord model, along with a corresponding table in your database:
+ ClassFactory :person
+
+Execute a migration on the new table specified as a block, defining the attributes of the new model class:
+ ClassFactory :person do |p|
+ p.string :first_name
+ p.string :last_name
+ p.integer :age
+ end
+
+Create a class with a specified superclass (default is ActiveRecord::Base):
+ ClassFactory :person_array, :super => Array
+If SuperClass is not a subclass of ActiveRecord::Base then Class Factory won't create a table or run a migration. You can use this to create plain Ruby object classes.
+
+Create a class called "DifferentClass" instead of "Person:"
+ ClassFactory :person, :class => 'DifferentClass'
+
+Run the given code inside the new class using class_eval:
+ ClassFactory :person, :class_eval => 'has_many :shoes'
+
+Create a table with the given name, instead of a table called "people:"
+ ClassFactory :person, :class_eval => 'set_table_name :table_name', :table => 'table_name'
+
+If you provide options when the factory is defined they will be applied to each class created with the factory. You can also provide options when you create a class, in which case they will override the factory options.
+
+== Detailed examples and more information
+
+See: {http://patshaughnessy.net/class_factory}[http://patshaughnessy.net/class_factory]
View
6 Rakefile
@@ -5,10 +5,10 @@ begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "class_factory"
- gem.summary = %Q{TODO: one-line summary of your gem}
- gem.description = %Q{TODO: longer description of your gem}
+ gem.summary = %Q{Class Factory: Factory_girl-like syntax for dynamically creating Ruby classes}
+ gem.description = %Q{Use syntax similar to factory_girl to create new ActiveRecord or other test classes (vs. instances of an existing model).}
gem.email = "pat@patshaughnessy.net"
- gem.homepage = "http://github.com/patshaughnessy/class_factory"
+ gem.homepage = "http://patshaughnessy.net/class_factory"
gem.authors = ["Pat Shaughnessy"]
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
View
1 VERSION
@@ -0,0 +1 @@
+0.1.0
View
57 class_factory.gemspec
@@ -0,0 +1,57 @@
+# Generated by jeweler
+# DO NOT EDIT THIS FILE DIRECTLY
+# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
+# -*- encoding: utf-8 -*-
+
+Gem::Specification.new do |s|
+ s.name = %q{class_factory}
+ s.version = "0.1.0"
+
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+ s.authors = ["Pat Shaughnessy"]
+ s.date = %q{2010-03-12}
+ s.description = %q{Use syntax similar to factory_girl to create new ActiveRecord or other test classes (vs. instances of an existing model).}
+ s.email = %q{pat@patshaughnessy.net}
+ s.extra_rdoc_files = [
+ "LICENSE",
+ "README.rdoc"
+ ]
+ s.files = [
+ ".document",
+ ".gitignore",
+ "LICENSE",
+ "README.rdoc",
+ "Rakefile",
+ "VERSION",
+ "class_factory.gemspec",
+ "lib/class_factory.rb",
+ "lib/class_factory/class_factory.rb",
+ "test/helper.rb",
+ "test/test_active_records.rb",
+ "test/test_plain_objects.rb"
+ ]
+ s.homepage = %q{http://patshaughnessy.net/class_factory}
+ s.rdoc_options = ["--charset=UTF-8"]
+ s.require_paths = ["lib"]
+ s.rubygems_version = %q{1.3.5}
+ s.summary = %q{Class Factory: Factory_girl-like syntax for dynamically creating Ruby classes}
+ s.test_files = [
+ "test/helper.rb",
+ "test/test_active_records.rb",
+ "test/test_plain_objects.rb"
+ ]
+
+ if s.respond_to? :specification_version then
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
+ s.specification_version = 3
+
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
+ s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
+ else
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
+ end
+ else
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
+ end
+end
+
View
7 lib/class_factory.rb
@@ -0,0 +1,7 @@
+require 'active_support'
+require 'active_record'
+require 'class_factory/class_factory'
+
+def ClassFactory(name, options = {}, &block)
+ ClassFactory.create(name, options, block)
+end
View
63 lib/class_factory/class_factory.rb
@@ -0,0 +1,63 @@
+class ClassFactory
+
+ class << self
+
+ attr_accessor :factories
+
+ def define(name, options = {}, &block)
+ factories[name] = self.new({ :name => name, :migration => block }.merge(options))
+ end
+
+ def create(name, options, block)
+ raise "No such class factory defined" if !factories.has_key?(name)
+ options.merge!(:migration => block) if block
+ factories[name].create(options)
+ end
+ end
+
+ def create(override_options)
+ @options = @definition.merge(override_options)
+ @options[:super] = ActiveRecord::Base if @options[:super].nil?
+ create_table if is_active_record?(@options[:super])
+ klass = create_class
+ klass.class_eval @options[:class_eval] if @options[:class_eval]
+ klass
+ end
+
+ private
+
+ def initialize(options)
+ @definition = options
+ end
+
+ def create_table
+ ActiveRecord::Base.connection.create_table table_name, :force => true do |table|
+ @options[:migration].call(table) unless @options[:migration].nil?
+ end
+ end
+
+ def create_class
+ Object.send(:remove_const, class_name) rescue nil
+ Object.const_set class_name, Class.new(@options[:super])
+ end
+
+ def is_active_record?(klass)
+ klass == ActiveRecord::Base || klass.ancestors.include?(ActiveRecord::Base)
+ end
+
+ def class_name
+ (@options[:class] || @options[:name]).to_s.camelize
+ end
+
+ def table_name
+ if @options[:table]
+ @options[:table]
+ else
+ (@options[:class] || @options[:name]).to_s.underscore.pluralize.to_sym
+ end
+ end
+
+ self.factories = {}
+
+end
+
View
3 test/helper.rb
@@ -1,6 +1,9 @@
require 'rubygems'
require 'test/unit'
require 'shoulda'
+require 'active_record'
+
+ActiveRecord::Base.establish_connection({ :adapter => 'sqlite3', :database => ':memory:' })
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
$LOAD_PATH.unshift(File.dirname(__FILE__))
View
124 test/test_active_records.rb
@@ -0,0 +1,124 @@
+require 'helper'
+
+class TestClassFactory < Test::Unit::TestCase
+
+ context "A class definition for a simple active record model" do
+ setup do
+ ClassFactory.define :model
+ end
+
+ should "create an ActiveRecord model by default" do
+ klass = ClassFactory :model
+ assert_equal Model, klass
+ assert_equal ActiveRecord::Base, klass.superclass
+ end
+
+ should "create a table for each new model with the correct name" do
+ ClassFactory :model
+ assert_nothing_raised do
+ ActiveRecord::Base.connection.execute('select * from models')
+ end
+ end
+
+ should "create a table for each new model with the correct name if a different class name is specified" do
+ ClassFactory :model, :class => 'different_class'
+ assert_nothing_raised do
+ ActiveRecord::Base.connection.execute('select * from different_classes')
+ end
+ end
+ end
+
+ context "A class definition for an active record model with a migration specified" do
+ setup do
+ ClassFactory.define :person do |p|
+ p.string :first_name
+ p.string :last_name
+ p.integer :age
+ end
+ ClassFactory :person
+ end
+
+ should "run the given migration on the new table" do
+ assert_nothing_raised do
+ ActiveRecord::Base.connection.execute('select first_name from people')
+ end
+ end
+
+ should "create an ActiveRecord model class that can be used in the normal way to insert and select data" do
+ assert_equal 0, Person.count
+ Person.create :first_name => 'Joe', :last_name => 'Blow', :age => 50
+ assert_equal 1, Person.count
+ assert_equal 'Joe', Person.first.first_name
+ assert_equal 50, Person.find_by_age(50).age
+ end
+
+ should "delete the table and its contents if the model class is redefined" do
+ Person.create :first_name => 'Joe', :last_name => 'Blow', :age => 50
+ assert_equal 1, Person.count
+ ClassFactory :person
+ assert_equal 0, Person.count
+ end
+ end
+
+ context "A class definition for an active record model with a certain table name specified" do
+ setup do
+ ClassFactory.define :model, :table => 'model_table' do |m|
+ m.string :name
+ end
+ end
+
+ should "create a table with the specified name" do
+ ClassFactory :model
+ assert_nothing_raised do
+ ActiveRecord::Base.connection.execute('select name from model_table')
+ end
+ end
+
+ should "allow the table name be overriden" do
+ ClassFactory :model, :table => 'table_for_models'
+ assert_nothing_raised do
+ ActiveRecord::Base.connection.execute('select name from table_for_models')
+ end
+ end
+
+ should "not change the original definition after overriding the table setting once" do
+ ClassFactory :model
+ ActiveRecord::Base.connection.execute('drop table model_table')
+ ClassFactory :model, :table => 'table_for_models'
+ ActiveRecord::Base.connection.execute('drop table table_for_models')
+ ClassFactory :model
+ assert_nothing_raised do
+ ActiveRecord::Base.connection.execute('select name from model_table')
+ end
+ end
+ end
+
+ context "A class definition with a default migration specified" do
+ setup do
+ ClassFactory.define :person do |p|
+ p.string :first_name
+ p.string :last_name
+ p.integer :age
+ end
+ end
+
+ should "allow the migration to be overriden" do
+ ClassFactory :person do |p|
+ p.string :name
+ p.integer :age
+ p.integer :group_id
+ end
+ assert_equal ["id", "name", "age", "group_id"], Person.column_names
+ end
+
+ should "not change the original definition after overriding the migration" do
+ ClassFactory :person do |p|
+ p.string :name
+ p.integer :age
+ p.integer :group_id
+ end
+ ClassFactory :person
+ assert_equal ["id", "first_name", "last_name", "age"], Person.column_names
+ end
+ end
+end
View
7 test/test_class_factory.rb
@@ -1,7 +0,0 @@
-require 'helper'
-
-class TestClassFactory < Test::Unit::TestCase
- should "probably rename this file and start testing for real" do
- flunk "hey buddy, you should probably rename this file and start testing for real"
- end
-end
View
127 test/test_plain_objects.rb
@@ -0,0 +1,127 @@
+require 'helper'
+
+class TestClassFactory < Test::Unit::TestCase
+
+ should "raise an exception for an undefined class factory" do
+ assert_raise RuntimeError do
+ ClassFactory :unknown
+ end
+ end
+
+ should "create a Ruby class with the expected name and with the specified super class" do
+ ClassFactory.define :plain_object, :super => Object
+ klass = ClassFactory :plain_object
+ assert_equal PlainObject, klass
+ assert_equal Object, klass.superclass
+
+ ClassFactory.define :array_object, :super => Array
+ klass = ClassFactory :array_object
+ assert_equal ArrayObject, klass
+ assert_equal Array, klass.superclass
+ end
+
+ context "A class factory definition for simple Ruby class" do
+ setup do
+ ClassFactory.define :plain_object, :super => Object
+ ClassFactory :plain_object
+ PlainObject.class_eval do
+ def some_method
+ 'return value'
+ end
+ end
+ end
+
+ should "create a Ruby class that you can add methods to" do
+ assert_equal 'return value', PlainObject.new.some_method
+ end
+
+ should "allow you to redefine a class more than once" do
+ assert_equal 'return value', PlainObject.new.some_method
+ ClassFactory :plain_object
+ assert_raise NoMethodError do
+ PlainObject.new.some_method
+ end
+ end
+ end
+
+ context "A class factory definition for simple Ruby class with a class_eval option specified" do
+ setup do
+ ClassFactory.define :plain_object, :super => Object, :class_eval => <<END
+ def some_method
+ 'return value'
+ end
+END
+ end
+
+ should "execute that class eval code when the class is created" do
+ ClassFactory :plain_object
+ assert_equal 'return value', PlainObject.new.some_method
+ end
+
+ should "allow the class eval code to be overriden with different code" do
+ ClassFactory :plain_object, :class_eval => <<END
+ def some_method
+ 'a different return value'
+ end
+END
+ assert_equal 'a different return value', PlainObject.new.some_method
+ end
+
+ should "not change the original definition after overriding the class_eval setting" do
+ ClassFactory :plain_object, :class_eval => <<END
+ def some_method
+ 'return value'
+ end
+END
+ ClassFactory :plain_object
+ assert_equal 'return value', PlainObject.new.some_method
+ end
+ end
+
+ context "A class factory definition for simple Ruby class" do
+ setup do
+ ClassFactory.define :plain_object, :super => Object
+ end
+
+ should "allow you to override the super class when the class is created" do
+ klass = ClassFactory :plain_object, :super => Array
+ assert_equal PlainObject, klass
+ assert_equal Array, klass.superclass
+ end
+
+ should "not change the original definition after overriding the super class setting" do
+ ClassFactory :plain_object, :super => Array
+ klass = ClassFactory :plain_object
+ assert_equal PlainObject, klass
+ assert_equal Object, klass.superclass
+ end
+ end
+
+ context "A class factory definition for simple Ruby class with a class setting" do
+ setup do
+ ClassFactory.define :plain_object, :super => Object, :class => 'CertainClass'
+ end
+
+ should "create a class with the specified name" do
+ klass = ClassFactory :plain_object
+ assert_equal CertainClass, klass
+ end
+
+ should "allow you to override the default class name" do
+ klass = ClassFactory :plain_object, :class => 'DifferentClass'
+ assert_equal DifferentClass, klass
+ end
+
+ should "not change the original definition after overriding the class name setting" do
+ ClassFactory :plain_object, :class => 'DifferentClass'
+ klass = ClassFactory :plain_object
+ assert_equal CertainClass, klass
+ end
+
+ should "allow you to use a class name not in camel case" do
+ klass = ClassFactory :plain_object, :class => 'some_otherType_OfClass_name'
+ assert_equal SomeOtherTypeOfClassName, klass
+ end
+ end
+
+end

0 comments on commit b18c0cb

Please sign in to comment.