fixtures.rb 23.9 KB
Newer Older
D
Initial  
David Heinemeier Hansson 已提交
1 2
require 'erb'
require 'yaml'
3
require 'csv'
D
Initial  
David Heinemeier Hansson 已提交
4

5 6 7 8 9
module YAML #:nodoc:
  class Omap #:nodoc:
    def keys;   map { |k, v| k } end
    def values; map { |k, v| v } end
  end
10 11
end

12 13 14
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
end

15
# Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavours:
D
Initial  
David Heinemeier Hansson 已提交
16
#
17 18 19
#   1.  YAML fixtures
#   2.  CSV fixtures
#   3.  Single-file fixtures
D
Initial  
David Heinemeier Hansson 已提交
20
#
21
# = YAML fixtures
D
Initial  
David Heinemeier Hansson 已提交
22
#
23 24
# This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures
# in a non-verbose, humanly-readable format. It ships with Ruby 1.8.1+.
D
Initial  
David Heinemeier Hansson 已提交
25
#
D
David Heinemeier Hansson 已提交
26
# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed in the directory appointed
27 28 29
# by <tt>Test::Unit::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
# put your files in <your-rails-app>/test/fixtures/). The fixture file ends with the .yml file extension (Rails example:
# "<your-rails-app>/test/fixtures/web_sites.yml"). The format of a YAML fixture file looks like this:
D
Initial  
David Heinemeier Hansson 已提交
30
#
31 32 33 34
#   rubyonrails:
#     id: 1
#     name: Ruby on Rails
#     url: http://www.rubyonrails.org
D
Initial  
David Heinemeier Hansson 已提交
35
#
36 37 38 39 40
#   google:
#     id: 2
#     name: Google
#     url: http://www.google.com
#
41
# This YAML fixture file includes two fixtures.  Each YAML fixture (ie. record) is given a name and is followed by an
42
# indented list of key/value pairs in the "key: value" format.  Records are separated by a blank line for your viewing
43
# pleasure.
44
#
45 46 47 48 49 50 51 52 53 54 55 56 57 58
# Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type.  See http://yaml.org/type/omap.html
# for the specification.  You will need ordered fixtures when you have foreign key constraints on keys in the same table.
# This is commonly needed for tree structures.  Example:
#
#    --- !omap
#    - parent:
#        id:         1
#        parent_id:  NULL
#        title:      Parent
#    - child:
#        id:         2
#        parent_id:  1
#        title:      Child
#
59 60
# = CSV fixtures
#
D
David Heinemeier Hansson 已提交
61
# Fixtures can also be kept in the Comma Separated Value format. Akin to YAML fixtures, CSV fixtures are stored
D
David Heinemeier Hansson 已提交
62
# in a single file, but instead end with the .csv file extension (Rails example: "<your-rails-app>/test/fixtures/web_sites.csv")
63
#
64
# The format of this type of fixture file is much more compact than the others, but also a little harder to read by us
65 66 67 68 69 70 71 72 73 74 75
# humans.  The first line of the CSV file is a comma-separated list of field names.  The rest of the file is then comprised
# of the actual data (1 per line).  Here's an example:
#
#   id, name, url
#   1, Ruby On Rails, http://www.rubyonrails.org
#   2, Google, http://www.google.com
#
# Should you have a piece of data with a comma character in it, you can place double quotes around that value.  If you
# need to use a double quote character, you must escape it with another double quote.
#
# Another unique attribute of the CSV fixture is that it has *no* fixture name like the other two formats.  Instead, the
76
# fixture names are automatically generated by deriving the class name of the fixture file and adding an incrementing
77 78 79
# number to the end.  In our example, the 1st fixture would be called "web_site_1" and the 2nd one would be called
# "web_site_2".
#
80
# Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you
81
# have existing data somewhere already.
82 83
#
# = Single-file fixtures
84 85
#
# This type of fixtures was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats.
86
# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
# appointed by <tt>Test::Unit::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
# put your files in <your-rails-app>/test/fixtures/<your-model-name>/ -- like <your-rails-app>/test/fixtures/web_sites/ for the WebSite
# model).
#
# Each text file placed in this directory represents a "record".  Usually these types of fixtures are named without
# extensions, but if you are on a Windows machine, you might consider adding .txt as the extension.  Here's what the
# above example might look like:
#
#   web_sites/google
#   web_sites/yahoo.txt
#   web_sites/ruby-on-rails
#
# The file format of a standard fixture is simple.  Each line is a property (or column in db speak) and has the syntax
# of "name => value".  Here's an example of the ruby-on-rails fixture above:
#
#   id => 1
#   name => Ruby on Rails
#   url => http://www.rubyonrails.org
#
# = Using Fixtures
#
# Since fixtures are a testing construct, we use them in our unit and functional tests.  There are two ways to use the
D
David Heinemeier Hansson 已提交
109
# fixtures, but first let's take a look at a sample unit test found:
110 111 112 113 114 115
#
#   require 'web_site'
#
#   class WebSiteTest < Test::Unit::TestCase
#     def test_web_site_count
#       assert_equal 2, WebSite.count
116
#     end
117
#   end
118 119
#
# As it stands, unless we pre-load the web_site table in our database with two records, this test will fail.  Here's the
120 121 122 123 124 125
# easiest way to add fixtures to the database:
#
#   ...
#   class WebSiteTest < Test::Unit::TestCase
#     fixtures :web_sites # add more by separating the symbols with commas
#   ...
126
#
127
# By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here tho), we trigger
128
# the testing environment to automatically load the appropriate fixtures into the database before each test.  
D
David Heinemeier Hansson 已提交
129
# To ensure consistent data, the environment deletes the fixtures before running the load.
130 131 132 133 134 135 136 137 138
#
# In addition to being available in the database, the fixtures are also loaded into a hash stored in an instance variable
# of the test case.  It is named after the symbol... so, in our example, there would be a hash available called
# @web_sites.  This is where the "fixture name" comes into play.
#
# On top of that, each record is automatically "found" (using Model.find(id)) and placed in the instance variable of its name.
# So for the YAML fixtures, we'd get @rubyonrails and @google, which could be interrogated using regular Active Record semantics:
#
#   # test if the object created from the fixture data has the same attributes as the data itself
D
Initial  
David Heinemeier Hansson 已提交
139
#   def test_find
140
#     assert_equal @web_sites["rubyonrails"]["name"], @rubyonrails.name
D
Initial  
David Heinemeier Hansson 已提交
141 142
#   end
#
143 144
# As seen above, the data hash created from the YAML fixtures would have @web_sites["rubyonrails"]["url"] return
# "http://www.rubyonrails.org" and @web_sites["google"]["name"] would return "Google". The same fixtures, but loaded
D
David Heinemeier Hansson 已提交
145
# from a CSV fixture file, would be accessible via @web_sites["web_site_1"]["name"] == "Ruby on Rails" and have the individual
146
# fixtures available as instance variables @web_site_1 and @web_site_2.
D
Initial  
David Heinemeier Hansson 已提交
147
#
148 149 150 151 152 153
# If you do not wish to use instantiated fixtures (usually for performance reasons) there are two options.
#
#   - to completely disable instantiated fixtures:
#       self.use_instantiated_fixtures = false
#
#   - to keep the fixture instance (@web_sites) available, but do not automatically 'find' each instance:
154
#       self.use_instantiated_fixtures = :no_instances 
155
#
156 157 158 159 160 161 162 163 164 165
# Even if auto-instantiated fixtures are disabled, you can still access them
# by name via special dynamic methods. Each method has the same name as the
# model, and accepts the name of the fixture to instantiate:
#
#   fixtures :web_sites
#
#   def test_find
#     assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
#   end
#
166
# = Dynamic fixtures with ERb
D
Initial  
David Heinemeier Hansson 已提交
167
#
168 169
# Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can
# mix ERb in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like:
D
Initial  
David Heinemeier Hansson 已提交
170
#
171 172 173 174 175
# <% for i in 1..1000 %>
# fix_<%= i %>:
#   id: <%= i %>
#   name: guy_<%= 1 %>
# <% end %>
D
Initial  
David Heinemeier Hansson 已提交
176
#
D
David Heinemeier Hansson 已提交
177
# This will create 1000 very simple YAML fixtures.
178 179
#
# Using ERb, you can also inject dynamic values into your fixtures with inserts like <%= Date.today.strftime("%Y-%m-%d") %>.
180
# This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable
181 182
# 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.
183 184 185
#
# = Transactional fixtures
#
186
# TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case. 
187 188 189 190 191
# 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
192
#   
193
#     fixtures :foos
194
#   
195
#     def test_godzilla
196
#       assert !Foo.find(:all).empty?
197
#       Foo.destroy_all
198
#       assert Foo.find(:all).empty?
199
#     end
200
#   
201
#     def test_godzilla_aftermath
202
#       assert !Foo.find(:all).empty?
203 204
#     end
#   end
205 206
#   
# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures, 
207 208
# 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.
#
209
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide 
210 211
# access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+)
#
212 213 214 215 216
# 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. 
217
#      Use InnoDB, MaxDB, or NDB instead.
218
class Fixtures < YAML::Omap
219 220
  DEFAULT_FILTER_RE = /\.ya?ml$/

221
  def self.instantiate_fixtures(object, table_name, fixtures, load_instances=true)
222
    object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures
223
    if load_instances
224
      ActiveRecord::Base.silence do
225
        fixtures.each do |name, fixture|
226 227 228 229
          begin
            object.instance_variable_set "@#{name}", fixture.find
          rescue FixtureClassNotFound
            nil
230
          end
231
        end
232
      end
D
Initial  
David Heinemeier Hansson 已提交
233 234
    end
  end
235

236
  def self.instantiate_all_loaded_fixtures(object, load_instances=true)
237 238
    all_loaded_fixtures.each do |table_name, fixtures|
      Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances)
239
    end
240
  end
241
  
242
  cattr_accessor :all_loaded_fixtures
243
  self.all_loaded_fixtures = {}
244

245
  def self.create_fixtures(fixtures_directory, table_names, class_names = {})
246
    table_names = [table_names].flatten.map { |n| n.to_s }
247 248
    connection  = block_given? ? yield : ActiveRecord::Base.connection

249
    ActiveRecord::Base.silence do
250
      fixtures_map = {}
251

252
      fixtures = table_names.map do |table_name|
253
        fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s))
254
      end               
255

256
      all_loaded_fixtures.merge! fixtures_map  
257

258
      connection.transaction(Thread.current['open_transactions'] == 0) do
259 260
        fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
        fixtures.each { |fixture| fixture.insert_fixtures }
261

262
        # Cap primary key sequences to max(pk).
263
        if connection.respond_to?(:reset_pk_sequence!)
264
          table_names.each do |table_name|
265 266
            connection.reset_pk_sequence!(table_name)
          end
267 268
        end
      end
269

D
Initial  
David Heinemeier Hansson 已提交
270
      return fixtures.size > 1 ? fixtures : fixtures.first
271 272 273
    end
  end

274

275 276
  attr_reader :table_name

277
  def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
D
Initial  
David Heinemeier Hansson 已提交
278
    @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
279 280
    @class_name = class_name || 
                  (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
281
    @table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix
282 283
    @table_name = class_name.table_name if class_name.respond_to?(:table_name)
    @connection = class_name.connection if class_name.respond_to?(:connection)
D
Initial  
David Heinemeier Hansson 已提交
284
    read_fixture_files
285 286 287
  end

  def delete_existing_fixtures
288
    @connection.delete "DELETE FROM #{@table_name}", 'Fixture Delete'
289 290 291 292
  end

  def insert_fixtures
    values.each do |fixture|
293
      @connection.insert_fixture fixture, @table_name
294
    end
D
Initial  
David Heinemeier Hansson 已提交
295 296
  end

297

298
  private
D
Initial  
David Heinemeier Hansson 已提交
299
    def read_fixture_files
300
      if File.file?(yaml_file_path)
301
        # YAML fixtures
302 303 304 305 306
        yaml_string = ""
        Dir["#{@fixture_path}/**/*.yml"].select {|f| test(?f,f) }.each do |subfixture_path|
          yaml_string << IO.read(subfixture_path)
        end
        yaml_string << IO.read(yaml_file_path)
307

308
        begin
309
          yaml = YAML::load(erb_render(yaml_string))
310
        rescue Exception=>boom
311
          raise Fixture::FormatError, "a YAML error occurred parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n  #{boom.class}: #{boom}"
312 313
        end

314
        if yaml
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329
          # If the file is an ordered map, extract its children.
          yaml_value =
            if yaml.respond_to?(:type_id) && yaml.respond_to?(:value)
              yaml.value
            else
              [yaml]
            end

          yaml_value.each do |fixture|
            fixture.each do |name, data|
              unless data
                raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)"
              end

              self[name] = Fixture.new(data, @class_name)
330 331
            end
          end
D
Initial  
David Heinemeier Hansson 已提交
332
        end
333
      elsif File.file?(csv_file_path)
334 335 336 337 338 339 340 341 342
        # CSV fixtures
        reader = CSV::Reader.create(erb_render(IO.read(csv_file_path)))
        header = reader.shift
        i = 0
        reader.each do |row|
          data = {}
          row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
          self["#{Inflector::underscore(@class_name)}_#{i+=1}"]= Fixture.new(data, @class_name)
        end
343 344
      elsif File.file?(deprecated_yaml_file_path)
        raise Fixture::FormatError, ".yml extension required: rename #{deprecated_yaml_file_path} to #{yaml_file_path}"
D
Initial  
David Heinemeier Hansson 已提交
345
      else
346
        # Standard fixtures
347 348 349 350 351
        Dir.entries(@fixture_path).each do |file|
          path = File.join(@fixture_path, file)
          if File.file?(path) and file !~ @file_filter
            self[file] = Fixture.new(path, @class_name)
          end
D
Initial  
David Heinemeier Hansson 已提交
352 353 354 355 356
        end
      end
    end

    def yaml_file_path
357 358 359 360 361
      "#{@fixture_path}.yml"
    end

    def deprecated_yaml_file_path
      "#{@fixture_path}.yaml"
D
Initial  
David Heinemeier Hansson 已提交
362
    end
363 364 365 366

    def csv_file_path
      @fixture_path + ".csv"
    end
367

D
Initial  
David Heinemeier Hansson 已提交
368 369 370 371 372 373 374 375 376 377 378
    def yaml_fixtures_key(path)
      File.basename(@fixture_path).split(".").first
    end

    def erb_render(fixture_content)
      ERB.new(fixture_content).result
    end
end

class Fixture #:nodoc:
  include Enumerable
379 380 381 382
  class FixtureError < StandardError#:nodoc:
  end
  class FormatError < FixtureError#:nodoc:
  end
D
Initial  
David Heinemeier Hansson 已提交
383

384 385
  attr_reader :class_name

D
Initial  
David Heinemeier Hansson 已提交
386
  def initialize(fixture, class_name)
387 388 389 390 391 392
    case fixture
      when Hash, YAML::Omap
        @fixture = fixture
      when String
        @fixture = read_fixture_file(fixture)
      else
393
        raise ArgumentError, "Bad fixture argument #{fixture.inspect} during creation of #{class_name} fixture"
394 395
    end

D
Initial  
David Heinemeier Hansson 已提交
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
    @class_name = class_name
  end

  def each
    @fixture.each { |item| yield item }
  end

  def [](key)
    @fixture[key]
  end

  def to_hash
    @fixture
  end

  def key_list
412 413
    columns = @fixture.keys.collect{ |column_name| ActiveRecord::Base.connection.quote_column_name(column_name) }
    columns.join(", ")
D
Initial  
David Heinemeier Hansson 已提交
414 415 416
  end

  def value_list
417 418 419
    klass = @class_name.constantize rescue nil

    list = @fixture.inject([]) do |fixtures, (key, value)|
420
      col = klass.columns_hash[key] if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
421
      fixtures << ActiveRecord::Base.connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
422 423
    end
    list * ', '
D
Initial  
David Heinemeier Hansson 已提交
424
  end
425

D
Initial  
David Heinemeier Hansson 已提交
426
  def find
427 428
    klass = @class_name.is_a?(Class) ? @class_name : Object.const_get(@class_name) rescue nil
    if klass
429
      klass.find(self[klass.primary_key])
430 431
    else
      raise FixtureClassNotFound, "The class #{@class_name.inspect} was not found."
432
    end
D
Initial  
David Heinemeier Hansson 已提交
433
  end
434

D
Initial  
David Heinemeier Hansson 已提交
435 436 437 438
  private
    def read_fixture_file(fixture_file_path)
      IO.readlines(fixture_file_path).inject({}) do |fixture, line|
        # Mercifully skip empty lines.
439
        next if line =~ /^\s*$/
D
Initial  
David Heinemeier Hansson 已提交
440 441 442

        # Use the same regular expression for attributes as Active Record.
        unless md = /^\s*([a-zA-Z][-_\w]*)\s*=>\s*(.+)\s*$/.match(line)
443
          raise FormatError, "#{fixture_file_path}: fixture format error at '#{line}'.  Expecting 'key => value'."
D
Initial  
David Heinemeier Hansson 已提交
444 445 446 447
        end
        key, value = md.captures

        # Disallow duplicate keys to catch typos.
448
        raise FormatError, "#{fixture_file_path}: duplicate '#{key}' in fixture." if fixture[key]
D
Initial  
David Heinemeier Hansson 已提交
449 450 451 452 453 454
        fixture[key] = value.strip
        fixture
      end
    end
end

455 456
module Test #:nodoc:
  module Unit #:nodoc:
457
    class TestCase #:nodoc:
458
      class_inheritable_accessor :fixture_path
459
      class_inheritable_accessor :fixture_table_names
460
      class_inheritable_accessor :fixture_class_names
461
      class_inheritable_accessor :use_transactional_fixtures
462 463
      class_inheritable_accessor :use_instantiated_fixtures   # true, false, or :no_instances
      class_inheritable_accessor :pre_loaded_fixtures
464 465 466 467

      self.fixture_table_names = []
      self.use_transactional_fixtures = false
      self.use_instantiated_fixtures = true
468
      self.pre_loaded_fixtures = false
469 470 471
      
      self.fixture_class_names = {}
      
472
      @@already_loaded_fixtures = {}
473 474 475 476 477 478
      self.fixture_class_names = {}
      
      def self.set_fixture_class(class_names = {})
        self.fixture_class_names = self.fixture_class_names.merge(class_names)
      end
      
479
      def self.fixtures(*table_names)
480 481 482 483 484 485 486
        if table_names.first == :all
          table_names = Dir["#{fixture_path}/*.yml"] + Dir["#{fixture_path}/*.csv"]
          table_names.map! { |f| File.basename(f).split('.')[0..-2].join('.') }
        else
          table_names = table_names.flatten.map { |n| n.to_s }
        end

487 488 489 490
        self.fixture_table_names |= table_names
        require_fixture_classes(table_names)
        setup_fixture_accessors(table_names)
      end
491

492 493
      def self.require_fixture_classes(table_names=nil)
        (table_names || fixture_table_names).each do |table_name| 
494 495
          file_name = table_name.to_s
          file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
496
          begin
497
            require_dependency file_name
498 499
          rescue LoadError
            # Let's hope the developer has included it himself
500 501
          end
        end
502
      end
503

504 505 506
      def self.setup_fixture_accessors(table_names=nil)
        (table_names || fixture_table_names).each do |table_name|
          table_name = table_name.to_s.tr('.','_')
507

508 509 510 511
          define_method(table_name) do |fixture, *optionals|
            force_reload = optionals.shift
            @fixture_cache[table_name] ||= Hash.new
            @fixture_cache[table_name][fixture] = nil if force_reload
512

513 514 515 516 517
            if @loaded_fixtures[table_name][fixture.to_s]
              @fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
            else
              raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'"
            end
518 519
          end
        end
520
      end
521

522
      def self.uses_transaction(*methods)
J
Jeremy Kemper 已提交
523 524
        @uses_transaction = [] unless defined?(@uses_transaction)
        @uses_transaction.concat methods.map(&:to_s)
525
      end
526

527
      def self.uses_transaction?(method)
J
Jeremy Kemper 已提交
528 529
        @uses_transaction = [] unless defined?(@uses_transaction)
        @uses_transaction.include?(method.to_s)
530 531 532
      end

      def use_transactional_fixtures?
533 534
        use_transactional_fixtures &&
          !self.class.uses_transaction?(method_name)
535 536
      end

537
      def setup_with_fixtures
538
        return unless defined?(ActiveRecord::Base) && !ActiveRecord::Base.configurations.blank?
539

540
        if pre_loaded_fixtures && !use_transactional_fixtures
541
          raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' 
542 543
        end

544 545
        @fixture_cache = Hash.new

546
        # Load fixtures once and begin transaction.
547
        if use_transactional_fixtures?
548 549 550 551 552 553
          if @@already_loaded_fixtures[self.class]
            @loaded_fixtures = @@already_loaded_fixtures[self.class]
          else
            load_fixtures
            @@already_loaded_fixtures[self.class] = @loaded_fixtures
          end
554
          ActiveRecord::Base.send :increment_open_transactions
555 556 557 558
          ActiveRecord::Base.connection.begin_db_transaction

        # Load fixtures for every test.
        else
559 560
          @@already_loaded_fixtures[self.class] = nil
          load_fixtures
561 562 563 564 565 566 567 568 569
        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
570
        return unless defined?(ActiveRecord::Base) && !ActiveRecord::Base.configurations.blank?
571

572
        # Rollback changes if a transaction is active.
573
        if use_transactional_fixtures? && Thread.current['open_transactions'] != 0
574
          ActiveRecord::Base.connection.rollback_db_transaction
575
          Thread.current['open_transactions'] = 0
576
        end
R
Rick Olson 已提交
577
        ActiveRecord::Base.verify_active_connections!
D
Initial  
David Heinemeier Hansson 已提交
578 579
      end

580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598
      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
599 600 601
          end
        end
      end
602

603
      private
604 605
        def load_fixtures
          @loaded_fixtures = {}
606
          fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
607 608 609
          unless fixtures.nil?
            if fixtures.instance_of?(Fixtures)
              @loaded_fixtures[fixtures.table_name] = fixtures
610
            else
611 612
              fixtures.each { |f| @loaded_fixtures[f.table_name] = f }
            end
613
          end
614
        end
615

616 617
        # for pre_loaded_fixtures, only require the classes once. huge speed improvement
        @@required_fixture_classes = false
618

619
        def instantiate_fixtures
620 621 622
          if pre_loaded_fixtures
            raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty?
            unless @@required_fixture_classes
623
              self.class.require_fixture_classes Fixtures.all_loaded_fixtures.keys
624 625 626
              @@required_fixture_classes = true
            end
            Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
627
          else
628 629 630 631
            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, load_instances?)
            end
632
          end
633
        end
634

635 636 637
        def load_instances?
          use_instantiated_fixtures != :no_instances
        end
D
Initial  
David Heinemeier Hansson 已提交
638
    end
639

640
  end
641
end