Skip to content

Commit 903ef71

Browse files
author
David Heinemeier Hansson
committed
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]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@846 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
1 parent 0ceab81 commit 903ef71

File tree

3 files changed

+139
-29
lines changed

3 files changed

+139
-29
lines changed

activerecord/CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
*SVN*
22

3+
* 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]
4+
35
* Added destruction of dependent objects in has_one associations when a new assignment happens #742 [mindel]. Example:
46

57
class Account < ActiveRecord::Base

activerecord/lib/active_record/fixtures.rb

Lines changed: 107 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -138,16 +138,46 @@
138138
# This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable
139139
# sample data. If you feel that you need to inject dynamic values, then perhaps you should reexamine whether your application
140140
# is properly testable. Hence, dynamic values in fixtures are to be considered a code smell.
141+
#
142+
# = Transactional fixtures
143+
#
144+
# TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case.
145+
# They can also turn off auto-instantiation of fixture data since the feature is costly and often unused.
146+
#
147+
# class FooTest < Test::Unit::TestCase
148+
# self.use_transactional_fixtures = true
149+
# self.use_instantiated_fixtures = false
150+
#
151+
# fixtures :foos
152+
#
153+
# def test_godzilla
154+
# assert !Foo.find_all.emtpy?
155+
# Foo.destroy_all
156+
# assert Foo.find_all.emtpy?
157+
# end
158+
#
159+
# def test_godzilla_aftermath
160+
# assert !Foo.find_all.emtpy?
161+
# end
162+
# end
163+
#
164+
# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
165+
# 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.
166+
#
167+
# When *not* to use transactional fixtures:
168+
# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
169+
# particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
170+
# the results of your transaction until Active Record supports nested transactions or savepoints (in progress.)
171+
# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
172+
# Use InnoDB, MaxDB, or NDB instead.
141173
class Fixtures < Hash
142174
DEFAULT_FILTER_RE = /\.ya?ml$/
143175

144-
def self.instantiate_fixtures(object, fixtures_directory, *table_names)
145-
[ create_fixtures(fixtures_directory, *table_names) ].flatten.each_with_index do |fixtures, idx|
146-
object.instance_variable_set "@#{table_names[idx]}", fixtures
147-
fixtures.each do |name, fixture|
148-
if model = fixture.find
149-
object.instance_variable_set "@#{name}", model
150-
end
176+
def self.instantiate_fixtures(object, table_name, fixtures)
177+
object.instance_variable_set "@#{table_name}", fixtures
178+
fixtures.each do |name, fixture|
179+
if model = fixture.find
180+
object.instance_variable_set "@#{name}", model
151181
end
152182
end
153183
end
@@ -322,51 +352,100 @@ def read_fixture_file(fixture_file_path)
322352
end
323353
end
324354

325-
module Test#:nodoc:
326-
module Unit#:nodoc:
355+
module Test #:nodoc:
356+
module Unit #:nodoc:
327357
class TestCase #:nodoc:
328358
include ClassInheritableAttributes
329359

330360
cattr_accessor :fixture_path
331-
cattr_accessor :fixture_table_names
361+
class_inheritable_accessor :fixture_table_names
362+
class_inheritable_accessor :use_transactional_fixtures
363+
class_inheritable_accessor :use_instantiated_fixtures
364+
365+
self.fixture_table_names = []
366+
self.use_transactional_fixtures = false
367+
self.use_instantiated_fixtures = true
332368

333369
def self.fixtures(*table_names)
334-
require_fixture_classes(table_names)
335-
write_inheritable_attribute("fixture_table_names", table_names)
370+
self.fixture_table_names = table_names.flatten
371+
require_fixture_classes
336372
end
337373

338-
def self.require_fixture_classes(table_names)
339-
table_names.each do |table_name|
374+
def self.require_fixture_classes
375+
fixture_table_names.each do |table_name|
340376
begin
341-
require(Inflector.singularize(table_name.to_s))
377+
require Inflector.singularize(table_name.to_s)
342378
rescue LoadError
343-
# Let's hope the developer is included it himself
379+
# Let's hope the developer has included it himself
344380
end
345381
end
346382
end
347383

348-
def setup
349-
instantiate_fixtures(*fixture_table_names) if fixture_table_names
384+
def setup_with_fixtures
385+
# Load fixtures once and begin transaction.
386+
if use_transactional_fixtures
387+
load_fixtures unless @already_loaded_fixtures
388+
@already_loaded_fixtures = true
389+
ActiveRecord::Base.lock_mutex
390+
ActiveRecord::Base.connection.begin_db_transaction
391+
392+
# Load fixtures for every test.
393+
else
394+
load_fixtures
395+
end
396+
397+
# Instantiate fixtures for every test if requested.
398+
instantiate_fixtures if use_instantiated_fixtures
399+
end
400+
401+
alias_method :setup, :setup_with_fixtures
402+
403+
def teardown_with_fixtures
404+
# Rollback changes.
405+
if use_transactional_fixtures
406+
ActiveRecord::Base.connection.rollback_db_transaction
407+
ActiveRecord::Base.unlock_mutex
408+
end
350409
end
351410

352-
def self.method_added(method_symbol)
353-
if method_symbol == :setup && !method_defined?(:setup_without_fixtures)
354-
alias_method :setup_without_fixtures, :setup
355-
define_method(:setup) do
356-
instantiate_fixtures(*fixture_table_names) if fixture_table_names
357-
setup_without_fixtures
411+
alias_method :teardown, :teardown_with_fixtures
412+
413+
def self.method_added(method)
414+
case method.to_s
415+
when 'setup'
416+
unless method_defined?(:setup_without_fixtures)
417+
alias_method :setup_without_fixtures, :setup
418+
define_method(:setup) do
419+
setup_with_fixtures
420+
setup_without_fixtures
421+
end
422+
end
423+
when 'teardown'
424+
unless method_defined?(:teardown_without_fixtures)
425+
alias_method :teardown_without_fixtures, :teardown
426+
define_method(:teardown) do
427+
teardown_without_fixtures
428+
teardown_with_fixtures
429+
end
358430
end
359431
end
360432
end
361433

362434
private
363-
def instantiate_fixtures(*table_names)
364-
Fixtures.instantiate_fixtures(self, fixture_path, *table_names)
435+
def load_fixtures
436+
@loaded_fixtures = {}
437+
fixture_table_names.each do |table_name|
438+
@loaded_fixtures[table_name] = Fixtures.create_fixtures(fixture_path, table_name)
439+
end
365440
end
366441

367-
def fixture_table_names
368-
self.class.read_inheritable_attribute("fixture_table_names")
442+
def instantiate_fixtures
443+
raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
444+
@loaded_fixtures.each do |table_name, fixtures|
445+
Fixtures.instantiate_fixtures(self, table_name, fixtures)
446+
end
369447
end
370448
end
449+
371450
end
372451
end

activerecord/test/fixtures_test.rb

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,34 @@ def test_dirty_dirty_yaml_file
105105
def test_empty_csv_fixtures
106106
assert_not_nil Fixtures.new( Account.connection, "accounts", File.dirname(__FILE__) + "/fixtures/naked/csv/accounts")
107107
end
108-
108+
end
109+
110+
111+
class FixturesWithoutInstantiationTest < Test::Unit::TestCase
112+
self.use_instantiated_fixtures = false
113+
fixtures :topics, :developers, :accounts
114+
115+
def test_without_complete_instantiation
116+
assert_nil @topics
117+
assert_nil @first
118+
end
119+
120+
def test_fixtures_from_root_yml_without_instantiation
121+
assert_nil @unknown
122+
end
123+
end
124+
125+
126+
class TransactionalFixturesTest < Test::Unit::TestCase
127+
self.use_transactional_fixtures = true
128+
fixtures :topics
129+
130+
def test_destroy
131+
assert_not_nil @first
132+
@first.destroy
133+
end
134+
135+
def test_destroy_just_kidding
136+
assert_not_nil @first
137+
end
109138
end

0 commit comments

Comments
 (0)