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