Ruby on Rails
FlexibleFixtures

NOTE: I recently discovered that this method of re-opening the fixtures module may not work as intended. Using the patch may be the only option until this is merged in to the Rails core. —canadaduane

Rather than assume your fixture files, database tables, model class names and fixture group names all match in every case, this fixture module will let you define multiple fixtures for the same table, or give you the flexibility to use alternate fixture files in your tests.

For example, you could create several fixture files for your users table:

signed_up_users.yml
admin_users.yml

And then in your unit or functional tests, you could load the appropriate files like this:

class MyUserTestSuite < Test::Unit::TestCase
  fixture :signed_up_users, :class_name => "User" 

  ...
end

class MyOtherTestSuite < Test::Unit::TestCase
  fixture :admin_users, :class_name => "User" 

  ...
end

Alternatively, (and assuming you set your fixture files up so that there is no overlap between primary key ids) you could load several fixture files in to one table:

class MyLastTestSuite < Test::Unit::TestCase
  fixture :signed_up_users, :class_name => "User" 
  fixture :admin_users, :class_name => "User" 

  def test_loaded_users
    assert_equal @signed_up_users.size, 3
    assert_equal @admin_users.size, 4
  end
end

So create a file called ‘fixtures.rb’ and put it in your Rails Application’s lib/ folder. Then in your environment.rb file, add the line require 'fixtures' near the end of the file. Here is the content of fixtures.rb:

require 'erb'
require 'yaml'
require 'csv'

# A FixtureGroup is a set of fixtures identified by a name.  Normally, this is the name of the
# corresponding table in the database.  For example, when you declare the use of fixtures in a
# TestUnit class, like so:
#   fixtures :users
# you are creating a FixtureGroup whose name is 'users', and whose defaults are set such that the
# +class_name+, +file_name+ and +table_name+ are guessed from the FixtureGroup's name.
class FixtureGroup
  attr_accessor :table_name, :class_name, :connection
  attr_reader :group_name, :file_name

  def initialize(file_name, optional_names = {})
    self.file_name = file_name
    self.group_name = optional_names[:group_name] || file_name
    if optional_names[:table_name]
      self.table_name = optional_names[:table_name]
      self.class_name = optional_names[:class_name] || Inflector.classify(@table_name.to_s.gsub('.','_'))
    elsif optional_names[:class_name]
      self.class_name = optional_names[:class_name]
      if Object.const_defined?(@class_name)
        model_class = Object.const_get(@class_name)
        self.table_name = model_class.table_name
      end
    end

    # In case either :table_name or :class_name was not set:
    self.table_name ||= @group_name.to_s
    self.class_name ||= Inflector.classify(@table_name.to_s.gsub('.','_'))
  end

  def file_name=(name)
    @file_name = name.to_s
  end

  def group_name=(name)
    @group_name = name.to_sym
  end

  def class_file_name
    Inflector.underscore(@class_name)
  end

  # Instantiate an array of FixtureGroup objects from an array of strings (table_names)
  def self.array_from_names(names)
    names.collect { |n| FixtureGroup.new(n) }
  end

  def hash
    @group_name.hash
  end

  def eql?(other)
    @group_name.eql? other.group_name
  end
end

class Fixtures < Hash
  DEFAULT_FILTER_RE = /\.ya?ml$/

  cattr_accessor :all_loaded_fixtures
  self.all_loaded_fixtures = {}

  attr_accessor :connection, :fixtures_directory, :file_filter
  attr_accessor :fixture_group

  def initialize(connection, fixtures_directory, fixture_group, file_filter = DEFAULT_FILTER_RE)
    @connection, @fixtures_directory = connection, fixtures_directory
    @fixture_group = fixture_group
    @file_filter = file_filter
    read_fixture_files
  end

  def delete_existing_fixtures
    @connection.delete "DELETE FROM #{@fixture_group.table_name}", 'Fixture Delete'
  end

  def insert_fixtures
    values.each do |fixture|
      @connection.execute "INSERT INTO #{@fixture_group.table_name} (#{fixture.key_list}) 
                           VALUES (#{fixture.value_list})", 'Fixture Insert'
    end
  end

  private
    def read_fixture_files
      if File.file?(yaml_file_path)
        read_yaml_fixture_files
      elsif File.file?(csv_file_path)
        read_csv_fixture_files
      elsif File.file?(deprecated_yaml_file_path)
        raise Fixture::FormatError, ".yml extension required: rename #{deprecated_yaml_file_path} to #{yaml_file_path}" 
      elsif File.directory?(File.join(@fixtures_directory, @fixture_group.file_name))
        read_standard_fixture_files
      else
        raise Fixture::FixtureError, "Couldn't find a yaml, csv or standard file to load at #{@fixtures_directory}." 
      end
    end

    def read_yaml_fixture_files
      # YAML fixtures
      begin
        yaml = YAML::load(erb_render(IO.read(yaml_file_path)))
        yaml.each { |name, data| self[name] = Fixture.new(data, @fixture_group.class_name) } if yaml
      rescue Exception=>boom
        raise Fixture::FormatError, "a YAML error occured parsing #{yaml_file_path}. Please note that YAML
          must be consistently indented using spaces. Tabs are not allowed. Please have a look at
          <a href="http://www.yaml.org/faq.html">http://www.yaml.org/faq.html</a>\nThe exact error was:\n  #{boom.class}: #{boom}" 
      end
    end

    def read_csv_fixture_files
      # 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["#{fixture_group.class_file_name}_#{i+=1}"]= Fixture.new(data, @fixture_group.class_name)
      end
    end

    def read_standard_fixture_files
      # Standard fixtures
      path = File.join(@fixtures_directory, @fixture_group.file_name)
      Dir.entries(path).each do |file|
        path = File.join(@fixtures_directory, file)
        if File.file?(path) and file !~ @file_filter
          self[file] = Fixture.new(path, @fixture_group.class_name)
        end
      end
    end

    def yaml_file_path
      fixture_path_with_extension ".yml" 
    end

    def deprecated_yaml_file_path
      fixture_path_with_extension ".yaml" 
    end

    def csv_file_path
      fixture_path_with_extension ".csv" 
    end

    def fixture_path_with_extension(ext)
      File.join(@fixtures_directory, @fixture_group.file_name + ext)
    end      

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

    #def yaml_fixtures_key(path)
    #  File.basename(@fixture_path).split(".").first
    #end

  public
    class << self
      def instantiate_fixtures(object, fixture_group_name, fixtures, load_instances=true)
        old_logger_level = ActiveRecord::Base.logger.level
        ActiveRecord::Base.logger.level = Logger::ERROR

        # table_name.to_s.gsub('.','_') replaced by 'fixture_group_name'
        object.instance_variable_set "@#{fixture_group_name}", fixtures
        if load_instances
          fixtures.each do |name, fixture|
            if model = fixture.find
              object.instance_variable_set "@#{name}", model
            end
          end
        end

        ActiveRecord::Base.logger.level = old_logger_level
      end

      def instantiate_all_loaded_fixtures(object, load_instances=true)
        all_loaded_fixtures.each do |fixture_group_name, fixtures|
          Fixtures.instantiate_fixtures(object, fixture_group_name, fixtures, load_instances)
        end
      end

      def create_fixtures(fixtures_directory, *fixture_groups)
        connection = block_given? ? yield : ActiveRecord::Base.connection
        old_logger_level = ActiveRecord::Base.logger.level
        fixture_groups.flatten!

        # Backwards compatibility: Allow an array of table names to be passed in, but just use them
        # to create an array of FixtureGroup objects
        if not fixture_groups.empty? and fixture_groups.first.is_a?(String)
          fixture_groups = FixtureGroup.array_from_names(fixture_groups)
        end

        begin
          ActiveRecord::Base.logger.level = Logger::ERROR

          fixtures_map = {}
          fixtures = fixture_groups.map do |group|
            fixtures_map[group.group_name] = Fixtures.new(connection, fixtures_directory, group)
          end               
          # Make sure all refs to all_loaded_fixtures use group_name as hash index, not table_name
          all_loaded_fixtures.merge! fixtures_map  

          connection.transaction do
            fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
            fixtures.each { |fixture| fixture.insert_fixtures }
          end

          reset_sequences(connection, fixture_groups) if connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)

          return fixtures.size > 1 ? fixtures : fixtures.first
        ensure
          ActiveRecord::Base.logger.level = old_logger_level
        end
      end

      # Work around for PostgreSQL to have new fixtures created from id 1 and running.
      def reset_sequences(connection, fixture_groups)
        fixture_groups.flatten.each do |group|
          if Object.const_defined?(group.class_name)
            pk = eval("#{group.class_name}::primary_key")
            if pk == 'id'
              connection.execute(
                "SELECT setval('#{group.table_name}_id_seq', (SELECT MAX(id) FROM #{group.table_name}), true)", 
                'Setting Sequence'
              )
            end
          end
        end
      end
    end
end

class Fixture #:nodoc:
  include Enumerable
  class FixtureError < StandardError#:nodoc:
  end
  class FormatError < FixtureError#:nodoc:
  end

  def initialize(fixture, class_name)
    @fixture = fixture.is_a?(Hash) ? fixture : read_fixture_file(fixture)
    @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
    columns = @fixture.keys.collect{ |column_name| ActiveRecord::Base.connection.quote_column_name(column_name) }
    columns.join(", ")
  end

  def value_list
    @fixture.values.map { |v| ActiveRecord::Base.connection.quote(v).gsub('\\n', "\n").gsub('\\r', "\r") }.join(", ")
  end

  def find
    if Object.const_defined?(@class_name)
      klass = Object.const_get(@class_name)
      klass.find(self[klass.primary_key])
    end
  end

  private
    def read_fixture_file(fixture_file_path)
      IO.readlines(fixture_file_path).inject({}) do |fixture, line|
        # Mercifully skip empty lines.
        next if line =~ /^\s*$/

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

        # Disallow duplicate keys to catch typos.
        raise FormatError, "#{fixture_file_path}: duplicate '#{key}' in fixture." if fixture[key]
        fixture[key] = value.strip
        fixture
      end
    end
end

module Test #:nodoc:
  module Unit #:nodoc:
    class TestCase #:nodoc:
      include ClassInheritableAttributes

      cattr_accessor :fixtures_directory
      class_inheritable_accessor :fixture_groups
      class_inheritable_accessor :use_transactional_fixtures
      class_inheritable_accessor :use_instantiated_fixtures   # true, false, or :no_instances
      class_inheritable_accessor :pre_loaded_fixtures

      self.fixture_groups = []
      self.use_transactional_fixtures = false
      self.use_instantiated_fixtures = true
      self.pre_loaded_fixtures = false

      @@already_loaded_fixtures = {}

      # Backwards compatibility
      def self.fixture_path=(path); self.fixtures_directory = path; end
      def self.fixture_path; self.fixtures_directory; end
      # It would be more appropriate to call this 'fixture_group_names', but it's for back compat.
      def fixture_table_names; fixture_groups.collect { |g| g.group_name }; end

      def self.fixture(file_name, options = {})
        self.fixture_groups |= [FixtureGroup.new(file_name, options)]
        require_fixture_classes
        setup_fixture_accessors
      end

      def self.fixtures(*file_names)
        self.fixture_groups |= FixtureGroup.array_from_names(file_names.flatten)
        require_fixture_classes
        setup_fixture_accessors
      end

      def self.require_fixture_classes(override_fixture_groups=nil)
        (override_fixture_groups || fixture_groups).each do |group| 
          begin
            require group.class_file_name
          rescue LoadError
            # Let's hope the developer has included it himself
          end
        end
      end

      def self.setup_fixture_accessors(override_fixture_groups=nil)
        (override_fixture_groups || fixture_groups).each do |group|
          # table_name = table_name.to_s.tr('.','_')
          define_method(group.group_name) do |fixture, *optionals|
            force_reload = optionals.shift
            @fixture_cache[group.group_name] ||= Hash.new
            @fixture_cache[group.group_name][fixture] = nil if force_reload
            @fixture_cache[group.group_name][fixture] ||= @loaded_fixtures[group.group_name][fixture.to_s].find
          end
        end
      end

      def self.uses_transaction(*methods)
        @uses_transaction ||= []
        @uses_transaction.concat methods.map { |m| m.to_s }
      end

      def self.uses_transaction?(method)
        @uses_transaction && @uses_transaction.include?(method.to_s)
      end

      def use_transactional_fixtures?
        use_transactional_fixtures &&
          !self.class.uses_transaction?(method_name)
      end

      def setup_with_fixtures
        if pre_loaded_fixtures && !use_transactional_fixtures
          raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' 
        end

        @fixture_cache = Hash.new

        # Load fixtures once and begin transaction.
        if use_transactional_fixtures?
          if @@already_loaded_fixtures[self.class]
            @loaded_fixtures = @@already_loaded_fixtures[self.class]
          else
            load_fixtures
            @@already_loaded_fixtures[self.class] = @loaded_fixtures
          end
          ActiveRecord::Base.lock_mutex
          ActiveRecord::Base.connection.begin_db_transaction

        # Load fixtures for every test.
        else
          @@already_loaded_fixtures[self.class] = nil
          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

      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 load_fixtures
          @loaded_fixtures = {}
          fixtures = Fixtures.create_fixtures(fixtures_directory, fixture_groups)
          unless fixtures.nil?
            if fixtures.instance_of?(Fixtures)
              @loaded_fixtures[fixtures.fixture_group.group_name] = fixtures
            else
              fixtures.each { |f| @loaded_fixtures[f.fixture_group.group_name] = f }
            end
          end
        end

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

        def instantiate_fixtures
          if pre_loaded_fixtures
            raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty?
            unless @@required_fixture_classes
              groups = Fixtures.all_loaded_fixtures.values.collect { |f| f.group_name }
              self.class.require_fixture_classes groups
              @@required_fixture_classes = true
            end
            Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
          else
            raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
            @loaded_fixtures.each do |fixture_group_name, fixtures|
              Fixtures.instantiate_fixtures(self, fixture_group_name, fixtures, load_instances?)
            end
          end
        end

        def load_instances?
          use_instantiated_fixtures != :no_instances
        end
    end

  end
end