Permalink
Browse files

Added transactional fixtures that uses rollback to undo changes to fi…

…xtures instead of DELETE/INSERT -- it's much faster. See documentation under Fixtures #760 [bitsweat]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@846 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
1 parent 0ceab81 commit 903ef71b9952f4bfaef798bbd93a972fc25010ad @dhh dhh committed Mar 6, 2005
Showing with 139 additions and 29 deletions.
  1. +2 −0 activerecord/CHANGELOG
  2. +107 −28 activerecord/lib/active_record/fixtures.rb
  3. +30 −1 activerecord/test/fixtures_test.rb
View
@@ -1,5 +1,7 @@
*SVN*
+* Added transactional fixtures that uses rollback to undo changes to fixtures instead of DELETE/INSERT -- it's much faster. See documentation under Fixtures #760 [bitsweat]
+
* Added destruction of dependent objects in has_one associations when a new assignment happens #742 [mindel]. Example:
class Account < ActiveRecord::Base
@@ -138,16 +138,46 @@
# This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable
# sample data. If you feel that you need to inject dynamic values, then perhaps you should reexamine whether your application
# is properly testable. Hence, dynamic values in fixtures are to be considered a code smell.
+#
+# = Transactional fixtures
+#
+# TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case.
+# They can also turn off auto-instantiation of fixture data since the feature is costly and often unused.
+#
+# class FooTest < Test::Unit::TestCase
+# self.use_transactional_fixtures = true
+# self.use_instantiated_fixtures = false
+#
+# fixtures :foos
+#
+# def test_godzilla
+# assert !Foo.find_all.emtpy?
+# Foo.destroy_all
+# assert Foo.find_all.emtpy?
+# end
+#
+# def test_godzilla_aftermath
+# assert !Foo.find_all.emtpy?
+# end
+# end
+#
+# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
+# then you may omit all fixtures declarations in your test cases since all the data's already there and every case rolls back its changes.
+#
+# When *not* to use transactional fixtures:
+# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
+# particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
+# the results of your transaction until Active Record supports nested transactions or savepoints (in progress.)
+# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
+# Use InnoDB, MaxDB, or NDB instead.
class Fixtures < Hash
DEFAULT_FILTER_RE = /\.ya?ml$/
- def self.instantiate_fixtures(object, fixtures_directory, *table_names)
- [ create_fixtures(fixtures_directory, *table_names) ].flatten.each_with_index do |fixtures, idx|
- object.instance_variable_set "@#{table_names[idx]}", fixtures
- fixtures.each do |name, fixture|
- if model = fixture.find
- object.instance_variable_set "@#{name}", model
- end
+ def self.instantiate_fixtures(object, table_name, fixtures)
+ object.instance_variable_set "@#{table_name}", fixtures
+ fixtures.each do |name, fixture|
+ if model = fixture.find
+ object.instance_variable_set "@#{name}", model
end
end
end
@@ -322,51 +352,100 @@ def read_fixture_file(fixture_file_path)
end
end
-module Test#:nodoc:
- module Unit#:nodoc:
+module Test #:nodoc:
+ module Unit #:nodoc:
class TestCase #:nodoc:
include ClassInheritableAttributes
cattr_accessor :fixture_path
- cattr_accessor :fixture_table_names
+ class_inheritable_accessor :fixture_table_names
+ class_inheritable_accessor :use_transactional_fixtures
+ class_inheritable_accessor :use_instantiated_fixtures
+
+ self.fixture_table_names = []
+ self.use_transactional_fixtures = false
+ self.use_instantiated_fixtures = true
def self.fixtures(*table_names)
- require_fixture_classes(table_names)
- write_inheritable_attribute("fixture_table_names", table_names)
+ self.fixture_table_names = table_names.flatten
+ require_fixture_classes
end
- def self.require_fixture_classes(table_names)
- table_names.each do |table_name|
+ def self.require_fixture_classes
+ fixture_table_names.each do |table_name|
begin
- require(Inflector.singularize(table_name.to_s))
+ require Inflector.singularize(table_name.to_s)
rescue LoadError
- # Let's hope the developer is included it himself
+ # Let's hope the developer has included it himself
end
end
end
- def setup
- instantiate_fixtures(*fixture_table_names) if fixture_table_names
+ def setup_with_fixtures
+ # Load fixtures once and begin transaction.
+ if use_transactional_fixtures
+ load_fixtures unless @already_loaded_fixtures
+ @already_loaded_fixtures = true
+ ActiveRecord::Base.lock_mutex
+ ActiveRecord::Base.connection.begin_db_transaction
+
+ # Load fixtures for every test.
+ else
+ load_fixtures
+ end
+
+ # Instantiate fixtures for every test if requested.
+ instantiate_fixtures if use_instantiated_fixtures
+ end
+
+ alias_method :setup, :setup_with_fixtures
+
+ def teardown_with_fixtures
+ # Rollback changes.
+ if use_transactional_fixtures
+ ActiveRecord::Base.connection.rollback_db_transaction
+ ActiveRecord::Base.unlock_mutex
+ end
end
- def self.method_added(method_symbol)
- if method_symbol == :setup && !method_defined?(:setup_without_fixtures)
- alias_method :setup_without_fixtures, :setup
- define_method(:setup) do
- instantiate_fixtures(*fixture_table_names) if fixture_table_names
- setup_without_fixtures
+ alias_method :teardown, :teardown_with_fixtures
+
+ def self.method_added(method)
+ case method.to_s
+ when 'setup'
+ unless method_defined?(:setup_without_fixtures)
+ alias_method :setup_without_fixtures, :setup
+ define_method(:setup) do
+ setup_with_fixtures
+ setup_without_fixtures
+ end
+ end
+ when 'teardown'
+ unless method_defined?(:teardown_without_fixtures)
+ alias_method :teardown_without_fixtures, :teardown
+ define_method(:teardown) do
+ teardown_without_fixtures
+ teardown_with_fixtures
+ end
end
end
end
private
- def instantiate_fixtures(*table_names)
- Fixtures.instantiate_fixtures(self, fixture_path, *table_names)
+ def load_fixtures
+ @loaded_fixtures = {}
+ fixture_table_names.each do |table_name|
+ @loaded_fixtures[table_name] = Fixtures.create_fixtures(fixture_path, table_name)
+ end
end
- def fixture_table_names
- self.class.read_inheritable_attribute("fixture_table_names")
+ def instantiate_fixtures
+ raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
+ @loaded_fixtures.each do |table_name, fixtures|
+ Fixtures.instantiate_fixtures(self, table_name, fixtures)
+ end
end
end
+
end
end
@@ -105,5 +105,34 @@ def test_dirty_dirty_yaml_file
def test_empty_csv_fixtures
assert_not_nil Fixtures.new( Account.connection, "accounts", File.dirname(__FILE__) + "/fixtures/naked/csv/accounts")
end
-
+end
+
+
+class FixturesWithoutInstantiationTest < Test::Unit::TestCase
+ self.use_instantiated_fixtures = false
+ fixtures :topics, :developers, :accounts
+
+ def test_without_complete_instantiation
+ assert_nil @topics
+ assert_nil @first
+ end
+
+ def test_fixtures_from_root_yml_without_instantiation
+ assert_nil @unknown
+ end
+end
+
+
+class TransactionalFixturesTest < Test::Unit::TestCase
+ self.use_transactional_fixtures = true
+ fixtures :topics
+
+ def test_destroy
+ assert_not_nil @first
+ @first.destroy
+ end
+
+ def test_destroy_just_kidding
+ assert_not_nil @first
+ end
end

0 comments on commit 903ef71

Please sign in to comment.