require 'yaml'
require 'active_record/support/class_inheritable_attributes'
require 'active_record/support/inflector'
# Fixtures are a way of organizing data that you want to test against. Each fixture file is created as a row
# in the database and created as a hash with column names as keys and data as values. All of these fixture hashes
# are kept in an overall hash where they can be accessed by their file name.
#
# Example:
#
# Directory with the fixture files
#
# developers/
# david
# luke
# jamis
#
# The file +david+ then contains:
#
# id => 1
# name => David Heinemeier Hansson
# birthday => 1979-10-15
# profession => Systems development
#
# Now when we call @developers = Fixtures.new(ActiveRecord::Base.connection, "developers", "developers/") all three
# developers will get inserted into the "developers" table through the active Active Record connection (that must be setup
# before-hand). And we can now query the fixture data through the @developers hash, so @developers["david"]["name"]
# will return "David Heinemeier Hansson" and @developers["david"]["birthday"] will return Date.new(1979, 10, 15).
#
# This can then be used for comparison in a unit test. Something like:
#
# def test_find
# assert_equal @developers["david"]["name"], Developer.find(@developers["david"]["id"]).name
# end
#
# == Automatic fixture setup and instance variable availability
#
# Fixtures can also be automatically instantiated in instance variables relating to their names using the following style:
#
# class FixturesTest < Test::Unit::TestCase
# fixtures :developers # you can add more with comma separation
#
# def test_developers
# assert_equal 3, @developers.size # the container for all the fixtures is automatically set
# assert_kind_of Developer, @david # works like @developers["david"].find
# assert_equal "David Heinemeier Hansson", @david.name
# end
# end
#
# == YAML fixtures
#
# Additionally, fixtures supports yaml files. Like fixture files, these yaml files have a pre-defined format. The document
# must be formatted like this:
#
# name: david
# data:
# id: 1
# name: David Heinemeier Hansson
# birthday: 1979-10-15
# profession: Systems development
# ---
# name: steve
# data:
# id: 2
# name: Steve Ross Kellock
# birthday: 1974-09-27
# profession: guy with keyboard
#
# In that file, there's two records. Each record must have two parts: 'name' and 'data'. The data that you add
# must be indented like you see above.
#
# Yaml fixtures file names must end with .yml as in people.yml or camel.yml. The yaml fixtures are placed in the same
# directory as the normal fixtures and can happy co-exist. :)
class Fixtures < Hash
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 { |name, fixture| object.instance_variable_set "@#{name}", fixture.find }
end
end
def self.create_fixtures(fixtures_directory, *table_names)
connection = block_given? ? yield : ActiveRecord::Base.connection
old_logger_level = ActiveRecord::Base.logger.level
begin
ActiveRecord::Base.logger.level = Logger::ERROR
fixtures = connection.transaction do
table_names.flatten.map do |table_name|
Fixtures.new(connection, table_name.to_s, File.join(fixtures_directory, table_name.to_s))
end
end
return fixtures.size > 1 ? fixtures : fixtures.first
ensure
ActiveRecord::Base.logger.level = old_logger_level
end
end
def initialize(connection, table_name, fixture_path, file_filter = /^\.|CVS|\.yaml/)
@connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
@class_name = Inflector.classify(@table_name)
read_fixture_files
delete_existing_fixtures
insert_fixtures
end
private
def read_fixture_files
Dir.entries(@fixture_path).each do |file|
case file
when /\.ya?ml$/
path = File.join(@fixture_path, file)
YamlFixture.produce(path).each { |fixture|
self[fixture.yaml_name] = fixture
}
when @file_filter
# skip
else
self[file] = Fixture.new(@fixture_path, file, @class_name)
end
end
end
def delete_existing_fixtures
@connection.delete "DELETE FROM #{@table_name}"
end
def insert_fixtures
values.each do |fixture|
@connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES(#{fixture.value_list})"
end
end
end
class Fixture #:nodoc:
include Enumerable
class FixtureError < StandardError; end
class FormatError < FixtureError; end
def initialize(fixture_path, file, class_name)
@fixture_path, @file, @class_name = fixture_path, file, class_name
@fixture = read_fixture_file
@class_name
end
def each
@fixture.each { |item| yield item }
end
def [](key)
@fixture[key]
end
def to_hash
@fixture
end
def key_list
@fixture.keys.join(", ")
end
def value_list
@fixture.values.map { |v| ActiveRecord::Base.connection.quote(v).gsub('\\n', "\n").gsub('\\r', "\r") }.join(", ")
end
def find
Object.const_get(@class_name).find(self["id"])
end
private
def read_fixture_file
path = File.join(@fixture_path, @file)
IO.readlines(path).inject({}) do |fixture, line|
# Mercifully skip empty lines.
next if line.empty?
# Use the same regular expression for attributes as Active Record.
unless md = /^\s*([a-zA-Z][-_\w]*)\s*=>\s*(.+)\s*$/.match(line)
raise FormatError, "#{path}: fixture format error at '#{line}'. Expecting 'key => value'."
end
key, value = md.captures
# Disallow duplicate keys to catch typos.
raise FormatError, "#{path}: duplicate '#{key}' in fixture." if fixture[key]
fixture[key] = value.strip
fixture
end
end
end
# A YamlFixture is like a fixture, but instead of a name to use as
# a key, it uses a yaml_name.
class YamlFixture < Fixture #:nodoc:
class YamlFormatError < FormatError; end
# yaml_name is analogous to a normal fixture's filename
attr_reader :yaml_name
# Instantiate with fixture name and data.
def initialize(yaml_name, fixture)
@yaml_name, @fixture = yaml_name, fixture
end
def produce(yaml_file_name)
YamlFixture.produce(yaml_file_name)
end
# Extract an array of YamlFixtures from a yaml file.
def self.produce(yaml_file_name)
fixtures = []
File.open(yaml_file_name) do |yaml_file|
YAML::load_documents(yaml_file) do |doc|
missing = %w(name data).reject { |key| doc[key] }.join(' and ')
raise YamlFormatError, "#{path}: yaml fixture missing #{missing}: #{doc.to_yaml}" unless missing.empty?
fixtures << YamlFixture.new(doc['name'], doc['data'])
end
end
fixtures
end
end
class Test::Unit::TestCase #:nodoc:
include ClassInheritableAttributes
cattr_accessor :fixture_path
cattr_accessor :fixture_table_names
def self.fixtures(*table_names)
write_inheritable_attribute("fixture_table_names", table_names)
end
def setup
instantiate_fixtures(*fixture_table_names) if fixture_table_names
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
end
end
end
private
def instantiate_fixtures(*table_names)
Fixtures.instantiate_fixtures(self, fixture_path, *table_names)
end
def fixture_table_names
self.class.read_inheritable_attribute("fixture_table_names")
end
end