Skip to content

Commit

Permalink
Added transactional fixtures that uses rollback to undo changes to fi…
Browse files Browse the repository at this point in the history
…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
dhh committed Mar 6, 2005
1 parent 0ceab81 commit 903ef71
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 29 deletions.
2 changes: 2 additions & 0 deletions activerecord/CHANGELOG
@@ -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
Expand Down
135 changes: 107 additions & 28 deletions activerecord/lib/active_record/fixtures.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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
31 changes: 30 additions & 1 deletion activerecord/test/fixtures_test.rb
Expand Up @@ -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.