Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial import of fixture_dependencies plugin

  • Loading branch information...
commit 1f79b005e9b86c1ea53ab6db0fd1af4141c86b19 0 parents
@jeremyevans authored
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2007 Jeremy Evans
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
202 README
@@ -0,0 +1,202 @@
+fixture_dependencies
+====================
+
+fixture_dependencies is a plugin that changes the way Rails uses fixtures in
+the following ways:
+
+- Fixtures can specify associations instead of foreign keys
+- Supports belongs_to, has_many, has_one, and habtm associations
+- Loads a fixture's dependencies (associations with other fixtures) before the
+ fixture itself so that foreign key constraints aren't violated
+- Can specify individual fixtures to load per test or test suite
+- Loads fixtures on every test inside a transaction, so fixture information
+ is never left in your database
+- Handles almost all cyclic dependencies
+
+To use, first install the plugin, then add the following to
+test/test_helper.rb after "require 'test_help'":
+
+ require 'fixture_dependencies_test_help'
+
+This overrides the default test helper to load the fixtures inside transactions
+and to use FixtureDependencies to load the fixtures.
+
+Changes to Fixtures
+-------------------
+
+fixture_dependencies is designed to require the least possible changes to
+fixtures. For example, see the following changes:
+
+ OLD NEW
+ asset1: asset1:
+ id: 1 id: 1
+ employee_id: 2 employee: jeremy
+ product_id: 3 product: nx7010
+ vendor_id: 2 vendor: lxg_computers
+ note: in working order note: in working order
+
+As you can see, you just replace the foreign key attribute and value with the
+name of the association and the associations name. This assumes you have an
+employee fixture with a name of jeremy, and products fixture with the name of
+nx7010, and a vendors fixture with the name lxg_computers.
+
+Fixture files still use the table_name of the model.
+
+Changes to the fixtures Class Method
+------------------------------------
+
+fixture_dependencies can still use the fixtures class method in your test:
+
+ class EmployeeTest < Test::Unit::TestCase
+ fixtures :assets
+ end
+
+In Rails default testing practices, the arguments to fixtures are table names.
+fixture_dependencies changes this to underscored model names. If you are using
+Rails' recommended table practices, this shouldn't make a difference.
+
+Another change is that Rails defaults allow you to specify habtm join tables in
+fixtures. That doesn't work with fixture dependencies, as there is no
+associated model. Instead, you use a has_and_belongs_to_many association name
+in the the appropriate model fixtures (see below).
+
+Loading Individual Fixtures with fixtures
+-----------------------------------------
+
+There is support for loading individual fixtures (and just their dependencies),
+using the following syntax:
+
+ class EmployeeTest < Test::Unit::TestCase
+ fixtures :employee__jeremy # Note the double underscore
+ end
+
+This would load just the jeremy fixture and its dependencies. I find this is
+much better than loading all fixtures in most of my test suites.
+
+Loading Fixtures Inside Test Methods
+------------------------------------
+
+I find that it is often better to skip the use of the fixtures method entirely,
+and load the fixtures I want manually in each test method. This provides for
+the loosest coupling possible. Here's an example:
+
+ class EmployeeTest < Test::Unit::TestCase
+ def test_employee_name
+ # Load the fixture and return the Employee object
+ employee = load(:employee__jeremy)
+ # Test the employee
+ end
+
+ def test_award_statistics
+ # Load all fixtures in both tables
+ load(:employee_awards, :awards)
+ # Test the award_statistics method
+ # (which pulls data from the tables loaded above)
+ end
+ end
+
+Don't worry about loading the same fixture twice, if a fixture is already
+loaded, it won't attempt to load it again.
+
+has_* Assocations in Fixtures
+-----------------------------
+
+Here's an example of using has_one (logon_information), has_many (assets), and
+has_and_belongs_to_many (groups) associations.
+
+jeremy:
+ id: 2
+ name: Jeremy Evans
+ logon_information: jeremy
+ assets: [asset1, asset2, asset3]
+ groups: [group1]
+
+logon_information is a has_one association to another table which was split
+from the employees table due to database security requirements. Assets is a
+has_many association, where one employee is responsible for the asset.
+Employees can be a member of multiple groups, and each group can have multiple
+employees.
+
+For has_* associations, after fixture_dependencies saves jeremy, it will load
+and save logon_information (and its dependencies...), it will load each asset
+in the order specified (and their dependencies...), and it will load all of the
+groups in the order specified (and their dependencies...). Note that there
+is only a load order inside a specific association, associations are stored
+in the same hash as attributes and are loaded in an arbitrary order.
+
+Cyclic Dependencies
+-------------------
+
+fixture_dependencies handles almost all cyclic dependencies. It handles all
+has_many, has_one, and habtm cyclic dependencies. It handles all
+self-referential cyclic dependencies. It handles all belongs_to cyclic
+dependencies except the case where there is a NOT NULL or validates_presence of
+constraint on the cyclic dependency's foreign key.
+
+For example, a case that won't work is when employee belongs_to supervisor
+(with a NOT NULL or validates_presence_of constraint on supervisor_id), and
+john is karl's supervisor and karl is john's supervisor. Since you can't create
+john without a valid supervisor_id, you need to create karl first, but you
+can't create karl for the same reason (as john doesn't exist yet).
+
+There isn't a generic way to handle the belongs_to cyclic dependency, as far as
+I know. Deferring foreign key checks could work, but may not be enabled (and
+one of the main reasons to use the plugin is that it doesn't require them).
+For associations like the example above (employee's supervisor is also an
+employee), setting the foreign_key to the primary key and then changing it
+later is an option, but database checks may prevent it. For more complex
+cyclic dependencies involving multiple model classes (employee belongs_to
+division belongs_to head_of_division when the employee is a member of the
+division and also the head of the division), even that approach is not
+possible.
+
+Known Issues
+------------
+
+Currently, the plugin only supports yaml fixtures, but other types of fixtures
+would be fairly easy to add (send me a patch if you add support for another
+fixture type).
+
+The plugin is significantly slower than the default testing method, because it
+loads all fixtures inside of a transaction (one per test method), where Rails
+defaults to loading the fixtures once per test suite (outside of a
+transaction), and only deletes fixtures from a table when overwriting it with
+new fixtures. Rails actually did something similar starting with r2714, but it
+was rolled back in r2730 due to speed issues. See ticket #2404 on Rails' trac.
+
+Instantiated fixtures are not available with this plugin. Instead, you should
+use load(:model__fixture_name).
+
+Troubleshooting
+---------------
+
+If you run into problems with loading your fixtures, it can be difficult to see
+where the problems are. To aid in debugging an error, add the following to
+test/test_helper.rb:
+
+ FixtureDependencies.verbose = 2
+
+This will give a verbose description of the loading and saving of fixtures for
+every test, including the recursive loading of all dependencies.
+
+Similar Ideas
+-------------
+
+fixture_references is a similar plugin. It uses erb inside yaml, and uses the
+foreign key numbers inside of the association names, which leads me to believe
+it doesn't support has_* associations.
+
+Ticket #6424 on the Rails' trac also implements a similar idea, but it parses
+the associations and changes them to foreign keys, which leads me to believe it
+doesn't support has_* associations either.
+
+License
+-------
+
+fixture_dependencies is released under the MIT License. See the LICENSE file
+for details.
+
+Author
+------
+
+Jeremy Evans <code@jeremyevans.net>
14 Rakefile
@@ -0,0 +1,14 @@
+require 'rake'
+require 'rake/rdoctask'
+
+desc 'Default: generate RDoc.'
+task :default => :rdoc
+
+desc 'Generate documentation for the fixture_dependencies plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'FixtureDependencies'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
2  init.rb
@@ -0,0 +1,2 @@
+# Include hook code here
+require 'fixture_dependencies'
132 lib/fixture_dependencies.rb
@@ -0,0 +1,132 @@
+class FixtureDependencies
+ @fixtures = {}
+ @loaded = {}
+ @verbose = 0
+ class << self
+ attr_reader :fixtures, :loaded
+ attr_accessor :verbose
+ def add(model_name, name, attributes)
+ (fixtures[model_name.to_sym]||={})[name.to_sym] = attributes
+ end
+
+ def get(record)
+ model_name, name = split_name(record)
+ model = model_name.classify.constantize
+ model.find(fixtures[model_name.to_sym][name.to_sym][model.primary_key.to_sym])
+ end
+
+ def load_yaml(model_name)
+ YAML.load(File.read(File.join(Test::Unit::TestCase.fixture_path, "#{model_name.classify.constantize.table_name}.yml"))).each do |name, attributes|
+ symbol_attrs = {}
+ attributes.each{|k,v| symbol_attrs[k.to_sym] = v}
+ add(model_name.to_sym, name, symbol_attrs)
+ end
+ loaded[model_name.to_sym] = true
+ end
+
+ def load(*records)
+ ret = records.collect do |record|
+ model_name, name = split_name(record)
+ if name
+ use(record.to_sym)
+ else
+ model_name = model_name.singularize
+ unless loaded[model_name.to_sym]
+ puts "loading #{model_name}.yml" if verbose > 0
+ load_yaml(model_name)
+ end
+ fixtures[model_name.to_sym].keys.collect{|name| use("#{model_name}__#{name}".to_sym)}
+ end
+ end
+ records.length == 1 ? ret[0] : ret
+ end
+
+ def split_name(name)
+ name.to_s.split('__', 2)
+ end
+
+ def use(record, loading = [], procs = {})
+ spaces = " " * loading.length
+ puts "#{spaces}using #{record}" if verbose > 0
+ puts "#{spaces}load stack:#{loading.inspect}" if verbose > 1
+ loading.push(record)
+ model_name, name = split_name(record)
+ model = model_name.classify.constantize
+ unless loaded[model_name.to_sym]
+ puts "#{spaces}loading #{model.table_name}.yml" if verbose > 0
+ load_yaml(model_name)
+ end
+ raise ActiveRecord::RecordNotFound, "Couldn't use fixture #{record.inspect}" unless attributes = fixtures[model_name.to_sym][name.to_sym]
+ # return if object has already been loaded into the database
+ if existing_obj = model.send("find_by_#{model.primary_key}", attributes[model.primary_key.to_sym])
+ return existing_obj
+ end
+ obj = model.new
+ many_associations = []
+ attributes.each do |attr, value|
+ if reflection = model.reflect_on_association(attr.to_sym)
+ if reflection.macro == :belongs_to
+ dep_name = "#{reflection.klass.name.underscore}__#{value}".to_sym
+ if dep_name == record
+ # Self referential record, use primary key
+ puts "#{spaces}#{record}.#{attr}: belongs_to self-referential" if verbose > 1
+ attr = reflection.options[:foreign_key] || reflection.klass.table_name.classify.foreign_key
+ value = attributes[model.primary_key.to_sym]
+ elsif loading.include?(dep_name)
+ # Association cycle detected, set foreign key for this model afterward using procs
+ # This is will fail if the column is set to not null or validates_presence_of
+ puts "#{spaces}#{record}.#{attr}: belongs-to cycle detected:#{dep_name}" if verbose > 1
+ (procs[dep_name] ||= []) << Proc.new do |assoc|
+ m = model.find(attributes[model.primary_key.to_sym])
+ m.send("#{attr}=", assoc)
+ m.save!
+ end
+ value = nil
+ else
+ # Regular assocation, load it
+ puts "#{spaces}#{record}.#{attr}: belongs_to:#{dep_name}" if verbose > 1
+ use(dep_name, loading, procs)
+ value = get(dep_name)
+ end
+ elsif
+ many_associations << [attr, reflection, reflection.macro == :has_one ? [value] : value]
+ next
+ end
+ end
+ obj.send("#{attr}=", value)
+ end
+ puts "#{spaces}saving #{record}" if verbose > 1
+ obj.save!
+ loading.pop
+ # Update the circular references
+ if procs[record]
+ procs[record].each{|p| p.call(obj)}
+ procs.delete(record)
+ end
+ # Update the has_many and habtm associations
+ many_associations.each do |attr, reflection, values|
+ proxy = obj.send(attr)
+ values.each do |value|
+ dep_name = "#{reflection.klass.name.underscore}__#{value}".to_sym
+ if dep_name == record
+ # Self referential, add association
+ puts "#{spaces}#{record}.#{attr}: #{reflection.macro} self-referential" if verbose > 1
+ reflection.macro == :has_one ? (proxy = obj) : (proxy << obj)
+ elsif loading.include?(dep_name)
+ # Cycle Detected, add association to this object after saving other object
+ puts "#{spaces}#{record}.#{attr}: #{reflection.macro} cycle detected:#{dep_name}" if verbose > 1
+ (procs[dep_name] ||= []) << Proc.new do |assoc|
+ reflection.macro == :has_one ? (proxy = assoc) : (proxy << assoc unless proxy.include?(assoc))
+ end
+ else
+ # Regular association, add it
+ puts "#{spaces}#{record}.#{attr}: #{reflection.macro}:#{dep_name}" if verbose > 1
+ assoc = use(dep_name, loading, procs)
+ reflection.macro == :has_one ? (proxy = assoc) : (proxy << assoc unless proxy.include?(assoc))
+ end
+ end
+ end
+ obj
+ end
+ end
+end
31 lib/fixture_dependencies_test_help.rb
@@ -0,0 +1,31 @@
+module Test
+ module Unit
+ class TestCase
+ class << self
+ alias_method :stupid_method_added, :method_added
+ end
+ def self.method_added(x)
+ end
+
+ def setup_with_fixtures
+ ActiveRecord::Base.send :increment_open_transactions
+ ActiveRecord::Base.connection.begin_db_transaction
+ load_fixtures
+ end
+ alias_method :setup, :setup_with_fixtures
+
+ class << self
+ alias_method :method_added, :stupid_method_added
+ end
+
+ private
+ def load_fixtures
+ load(*fixture_table_names)
+ end
+
+ def load(*fixture)
+ FixtureDependencies.load(*fixture)
+ end
+ end
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.