From 5e3eaff5bb00c4d19d9ff2e80d32090e9515fe2c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 7 Dec 2004 10:24:13 +0000 Subject: Added CSV format for fixtures #272 [what-a-day] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@55 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/CHANGELOG | 2 + activerecord/lib/active_record/fixtures.rb | 179 +++++++++++++++++++++++------ 2 files changed, 145 insertions(+), 36 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index fe04e4773e..0d19cf3e44 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *CVS* +* Added CSV format for fixtures #272 [what-a-day]. (See the new and expanded documentation on fixtures for more information) + * Fixed fixtures using primary key fields called something else than "id" [dave] * Added proper handling of time fields that are turned into Time objects with the dummy date of 2000/1/1 [HariSeldon] diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 56dc6bf383..3911b51806 100755 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -1,54 +1,145 @@ require 'erb' require 'yaml' +require 'csv' 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. You normally have one YAML file with fixture -# definitions per model. They're just hashes of hashes with the first-level key being the name of fixture (try to keep -# that name unique across all fixtures in the system for reasons that will follow). The value to that key is a hash -# where the keys are column names and the values the fixture data you want to insert into it. Example for developers.yml: +# Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavours: # -# david: -# id: 1 -# name: David Heinemeier Hansson -# birthday: 1979-10-15 -# profession: Systems development +# 1. YAML fixtures +# 2. CSV fixtures +# 3. Single-file fixtures # -# steve: -# id: 2 -# name: Steve Ross Kellock -# birthday: 1974-09-27 -# profession: guy with keyboard +# = YAML fixtures # -# So this YAML file includes two fixtures. T +# 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+. # -# Now when we call @developers = Fixtures.create_fixtures(".", "developers") both 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). +# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which is place in the directory appointed +# by Test::Unit::TestCase.fixture_path=(path) (this is automatically configured for Rails, so you can just +# put your files in /test/fixtures/). The fixture file ends with the .yml file extension (Rails example: +# "/test/fixtures/web_sites.yml"). The format of a YAML fixture file looks like this: # -# In addition to getting the raw data, we can also get the Developer object by doing @developers["david"].find. This can then -# be used for comparison in a unit test. Something like: +# rubyonrails: +# id: 1 +# name: Ruby on Rails +# url: http://www.rubyonrails.org # +# google: +# id: 2 +# name: Google +# url: http://www.google.com +# +# This YAML fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and is followed by an +# indented list of key/value pairs in the "key: value" format. Records are separated by a blank line for your viewing +# pleasure. +# +# = CSV fixtures +# +# Fixtures can also be kept in the in the Comma Separated Value format. Akin to YAML fixtures, CSV fixtures are stored +# in a single file, but, instead end with the .csv file extension (Rails example: "/test/fixtures/web_sites.csv") +# +# The format of this tye of fixture file is much more compact than the others, but also a little harder to read by us +# 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 +# fixture names are automatically generated by deriving the class name of the fixture file and adding an incrementing +# 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". +# +# Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you +# have existing data somewhere already. +# +# = Single-file fixtures +# +# This type of fixtures was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats. +# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory +# appointed by Test::Unit::TestCase.fixture_path=(path) (this is automatically configured for Rails, so you can just +# put your files in /test/fixtures// -- like /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 +# fixtures, but first lets take a look at a sample unit test found: +# +# require 'web_site' +# +# class WebSiteTest < Test::Unit::TestCase +# def test_web_site_count +# assert_equal 2, WebSite.count +# end +# end +# +# As it stands, unless we pre-load the web_site table in our database with two records, this test will fail. Here's the +# easiest way to add fixtures to the database: +# +# ... +# class WebSiteTest < Test::Unit::TestCase +# fixtures :web_sites # add more by separating the symbols with commas +# ... +# +# By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here tho), we trigger +# the testing environment to automatically load the appropriate fixtures into the database before each test, and +# automatically delete them after each test. +# +# 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 # def test_find -# assert_equal @developers["david"]["name"], @developers["david"].find.name +# assert_equal @web_sites["rubyonrails"]["name"], @rubyonrails.name # end # -# Comparing that the data we have on the name is also what the object returns when we ask for it. +# 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 +# from a CSV fixture file would be accessible via @web_sites["web_site_1"]["name"] == "Ruby on Rails" and have the individual +# fixtures available as instance variables @web_site_1 and @web_site_2. # -# == Automatic fixture setup and instance variable availability +# = Dynamic fixtures with ERb # -# Fixtures can also be automatically instantiated in instance variables relating to their names using the following style: +# 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: # -# class FixturesTest < Test::Unit::TestCase -# fixtures :developers # you can add more with comma separation +# <% for i in 1..1000 %> +# fix_<%= i %>: +# id: <%= i %> +# name: guy_<%= 1 %> +# <% end %> # -# 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 +# This will create 1000 YAML very simple fixtures. +# +# Using ERb, you can also inject dynamic values into your fixtures with inserts like <%= Date.today.strftime("%Y-%m-%d") %>. +# This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable +# 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. 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| @@ -68,8 +159,8 @@ class Fixtures < Hash fixtures = table_names.flatten.map do |table_name| Fixtures.new(connection, table_name.to_s, File.join(fixtures_directory, table_name.to_s)) end - fixtures.reverse.each{ |fixture| fixture.delete_existing_fixtures } - fixtures.each{ |fixture| fixture.insert_fixtures } + fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } + fixtures.each { |fixture| fixture.insert_fixtures } end return fixtures.size > 1 ? fixtures : fixtures.first ensure @@ -77,7 +168,7 @@ class Fixtures < Hash end end - def initialize(connection, table_name, fixture_path, file_filter = /^\.|CVS|\.yml/) + def initialize(connection, table_name, fixture_path, file_filter = /^\.|CVS|\.yml|\.csv/) @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter @class_name = Inflector.classify(@table_name) @@ -97,10 +188,22 @@ class Fixtures < Hash private def read_fixture_files if File.exists?(yaml_file_path) + # YAML fixtures YAML::load(erb_render(IO.read(yaml_file_path))).each do |name, data| self[name] = Fixture.new(data, @class_name) end + elsif File.exists?(csv_file_path) + # 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 else + # Standard fixtures Dir.entries(@fixture_path).each do |file| self[file] = Fixture.new(File.join(@fixture_path, file), @class_name) unless file =~ @file_filter end @@ -110,6 +213,10 @@ class Fixtures < Hash def yaml_file_path @fixture_path + ".yml" end + + def csv_file_path + @fixture_path + ".csv" + end def yaml_fixtures_key(path) File.basename(@fixture_path).split(".").first -- cgit v1.2.3