├── install.rb ├── uninstall.rb ├── History.txt ├── init.rb ├── test └── fixture_scenarios_test.rb ├── Rakefile ├── MIT-LICENSE ├── tasks └── fixture_scenarios_tasks.rake ├── README └── lib └── fixture_scenarios.rb /install.rb: -------------------------------------------------------------------------------- 1 | # Install hook code here 2 | -------------------------------------------------------------------------------- /uninstall.rb: -------------------------------------------------------------------------------- 1 | # Uninstall hook code here 2 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | = TRUNK 2 | - added "scenarios_load_root_fixtures" option to set default root loading [Peter Williams] -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit/testcase' 2 | require 'test/unit/testsuite' 3 | require 'active_record/fixtures' 4 | 5 | require 'fixture_scenarios' -------------------------------------------------------------------------------- /test/fixture_scenarios_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | class FixtureScenariosTest < Test::Unit::TestCase 4 | # Replace this with your real tests. 5 | def test_this_plugin 6 | flunk 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | desc 'Default: run unit tests.' 6 | task :default => :test 7 | 8 | desc 'Test the fixture_scenarios plugin.' 9 | Rake::TestTask.new(:test) do |t| 10 | t.libs << 'lib' 11 | t.pattern = 'test/**/*_test.rb' 12 | t.verbose = true 13 | end 14 | 15 | desc 'Generate documentation for the fixture_scenarios plugin.' 16 | Rake::RDocTask.new(:rdoc) do |rdoc| 17 | rdoc.rdoc_dir = 'rdoc' 18 | rdoc.title = 'FixtureScenarios' 19 | rdoc.options << '--line-numbers' << '--inline-source' 20 | rdoc.rdoc_files.include('README') 21 | rdoc.rdoc_files.include('lib/**/*.rb') 22 | end 23 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 Thomas Preston-Werner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /tasks/fixture_scenarios_tasks.rake: -------------------------------------------------------------------------------- 1 | namespace :test do 2 | desc "Run the scenario tests in test/scenarios" 3 | Rake::TestTask.new(:scenarios => "db:test:prepare") do |t| 4 | t.libs << "test" 5 | t.pattern = 'test/scenario/**/*_test.rb' 6 | t.verbose = true 7 | end 8 | end 9 | 10 | desc 'Test all scenarios' 11 | task :test do 12 | Rake::Task["test:scenarios"].invoke rescue got_error = true 13 | end 14 | 15 | namespace :db do 16 | namespace :fixtures do 17 | task :load => :environment do 18 | require 'active_record/fixtures' 19 | ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym) 20 | fixture_files = ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(RAILS_ROOT, 'test', 'fixtures', '*.{yml,csv}')) 21 | fixture_files.map! { |file| File.basename(file, '.*') } 22 | Fixtures.create_fixtures('test/fixtures', fixture_files) 23 | end 24 | end 25 | 26 | namespace :scenario do 27 | desc 'Load the given scenario into the database. Requires SCENARIO=x. Specify ROOT=false to not load root fixtures.' 28 | task :load => :environment do 29 | require 'active_record/fixtures' 30 | require 'fixture_scenarios' 31 | ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym) 32 | 33 | fixture_path = RAILS_ROOT + '/test/fixtures/' 34 | scenario_name = ENV['SCENARIO'] 35 | root = ENV['ROOT'] == 'false' ? false : true 36 | 37 | # find the scenario directory 38 | scenario_path = Dir.glob("#{fixture_path}**/*").grep(Regexp.new("/#{scenario_name}$")).first 39 | scenario_path = scenario_path[fixture_path.length..scenario_path.length] 40 | scenario_dirs = scenario_path.split('/').unshift('') 41 | 42 | # collect the file paths from which to load 43 | scenario_paths = [] 44 | while !scenario_dirs.empty? 45 | unless !root && scenario_dirs.size == 1 46 | scenario_paths << fixture_path.chop + scenario_dirs.join('/') 47 | end 48 | scenario_dirs.pop 49 | end 50 | scenario_paths.reverse! 51 | 52 | # collect the list of yaml and ruby files 53 | yaml_files = [] 54 | ruby_files = [] 55 | scenario_paths.each do |path| 56 | yaml_files |= Dir.glob("#{path}/*.y{am,m}l") 57 | ruby_files |= Dir.glob("#{path}/*.rb") 58 | end 59 | 60 | fixture_file_names = {} 61 | ruby_file_names = [] 62 | fixture_table_names = [] 63 | fixture_class_names = {} 64 | 65 | # collect table names 66 | table_names = [] 67 | yaml_files.each do |file_path| 68 | file_name = file_path.split("/").last 69 | table_name = file_name[0..file_name.rindex('.') - 1] 70 | table_names << table_name 71 | fixture_file_names[table_name] ||= [] 72 | fixture_file_names[table_name] << file_path 73 | end 74 | 75 | # collect ruby files 76 | ruby_file_names |= ruby_files 77 | 78 | fixture_table_names |= table_names 79 | 80 | Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_file_names, ruby_file_names, fixture_class_names) 81 | 82 | # (ENV['SCENARIO'] ? ENV['SCENARIO'].split(/,/) : Dir.glob(File.join(RAILS_ROOT, 'test', 'fixtures', '*.{yml,csv}'))).each do |fixture_file| 83 | # Fixtures.create_fixtures('test/fixtures', File.basename(fixture_file, '.*')) 84 | end 85 | end 86 | end -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 Tom Preston-Werner 2 | 3 | =FixtureScenarios 4 | 5 | This plugin allows you to create "scenarios" which are collections of fixtures 6 | and ruby files that represent a context against which you can run tests. 7 | 8 | ==Disclaimer 9 | 10 | This software is in Beta. 11 | Send feedback to tom at rubyisawesome dot com or find me (mojombo) on irc.freenode.net. 12 | 13 | ==Installation 14 | 15 | FixtureScenarios should work on both 1.1.6 and edge rails. 16 | Currently you must install this plugin from the subversion repository 17 | 18 | script/plugin install http://fixture-scenarios.googlecode.com/svn/trunk/fixture_scenarios 19 | 20 | ==WARNING 21 | 22 | Because this plugin clears out fixture data between your test classes, you may 23 | see some of your tests fail after installation. If this occurs, look at your 24 | tests to see if you didn't actually load a required fixture for that test 25 | class. If you forgot to add it and your tests passed anyway (because of 26 | fixture contamination), just add the missing fixture(s) and you'll be good 27 | to go. 28 | 29 | ==The Basics 30 | 31 | To create a scenario, simply create a subdirectory under test/fixtures in your 32 | Rails app. The name of the subdirectory will be the name of the scenario. 33 | Inside this new directory, you can place fixture files, and/or Ruby files. 34 | 35 | [RAILS_ROOT] 36 | +-test/ 37 | +-fixtures/ 38 | +-brand_new_user/ 39 | +-users.yml 40 | 41 | # in users.yml 42 | borges: 43 | id: 1 44 | name: Jorge Luis Borges 45 | active: 1 46 | 47 | To load the scenario for testing, you simply use the +scenario+ method instead 48 | of the normal +fixtures+ method. 49 | 50 | require File.dirname(__FILE__) + '/../test_helper' 51 | 52 | class UserTest < Test::Unit::TestCase 53 | scenario :brand_new_user 54 | 55 | def test_should_be_active 56 | assert users(:borges).active? 57 | end 58 | end 59 | 60 | All of the fixtures placed into your scenario directory will be loaded when 61 | you invoke the +scenario+ method with your scenario name. In addition, any 62 | Ruby files you place in the scenario directory will be run after the fixtures. 63 | You can use a Ruby file to create non-database model instances, set up 64 | relationships between fixtures (instead of creating fixtures for the join 65 | tables), or replace fixtures entirely by creating your database items with 66 | Ruby code. 67 | 68 | In this example, +scenario+ will actually load all fixtures from the fixture 69 | directory *and* your scenario directory. This is useful if you have some 70 | fixtures (such as lookup data) that you'd like to have in most of your 71 | scenarios. To prevent the loading of fixtures in the fixtures root directory, 72 | use the :root option. This can be very useful if you still have tests 73 | using regular fixtures. 74 | 75 | scenario :brand_new_user, :root => false 76 | 77 | If you've just started FixtureScenarios on an existing project, adding 78 | :root => false to every scenario call will become tedious, so you can set 79 | the option globally in your test_helper.rb (inside the Test::Unit::TestCase 80 | class) like so: 81 | 82 | self.scenarios_load_root_fixtures = false 83 | 84 | To keep things DRY in your scenarios, you can extend or layer scenarios on top 85 | of each other. Following with our example, to create an "experienced user" 86 | scenario, we could create another subdirectory under the existing 87 | "brand_new_user" that would contain fixture/Ruby files that add upon the 88 | "brand new user" scenario. 89 | 90 | [RAILS_ROOT] 91 | +-test/ 92 | +-fixtures/ 93 | +-brand_new_user/ 94 | +-users.yml 95 | +-experienced_user/ 96 | +-articles.yml 97 | 98 | Now when you load the +experienced_user+ scenario, it will load any 99 | fixture/Ruby files in "fixtures", then in "brand_new_user", then in 100 | "experienced_user"! Building off of your existing scenarios keeps data 101 | redundancy to a minimum, and makes it easy to change data for multiple 102 | scenarios simultaneously. 103 | 104 | ==Testing your scenarios 105 | 106 | Scenarios represent your assumptions about a given context. If these 107 | assumptions are wrong, your tests will be inaccurate. Your scenarios should be 108 | unit tested along with the rest of your application. This plugin allows you to 109 | place scenario tests in a "scenario" directory under your "test" directory. 110 | 111 | [RAILS_ROOT] 112 | +-test/ 113 | +-scenario/ 114 | +-brand_new_user_test.rb 115 | +-experienced_user_test.rb 116 | 117 | You can run these tests with rake. 118 | 119 | rake test:scenarios # run just scenario tests 120 | rake # run unit, functional, integration, and scenario tests 121 | 122 | Scenario tests will protect you from accidentally changing your assumptions in 123 | a dangerous or transparent way when modifying existing scenarios. -------------------------------------------------------------------------------- /lib/fixture_scenarios.rb: -------------------------------------------------------------------------------- 1 | # FixtureScenarios 2 | 3 | class Class 4 | private 5 | 6 | # Rails' class inheritable accessors are all broken due to a bad inherit implementation. 7 | # This method will override the bad one with one that actually works. 8 | def inherited_with_inheritable_attributes(child) 9 | inherited_without_inheritable_attributes(child) if respond_to?(:inherited_without_inheritable_attributes) 10 | 11 | new_inheritable_attributes = {} 12 | inheritable_attributes.each do |key, value| 13 | new_inheritable_attributes[key] = value.dup rescue value 14 | end 15 | 16 | child.instance_variable_set('@inheritable_attributes', new_inheritable_attributes) 17 | end 18 | 19 | alias inherited inherited_with_inheritable_attributes 20 | end 21 | 22 | class Fixtures < YAML::Omap 23 | def self.create_fixtures(fixtures_directory, table_names, file_names = {}, ruby_files = [], class_names = {}) 24 | table_names = [table_names].flatten.map { |n| n.to_s } 25 | connection = block_given? ? yield : ActiveRecord::Base.connection 26 | ActiveRecord::Base.silence do 27 | fixtures_map = {} 28 | fixtures = table_names.map do |table_name| 29 | fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], file_names[table_name] || [File.join(fixtures_directory, table_name.to_s)]) 30 | end 31 | all_loaded_fixtures.merge! fixtures_map 32 | 33 | connection.transaction(Thread.current['open_transactions'] == 0) do 34 | fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } 35 | fixtures.each { |fixture| fixture.insert_fixtures } 36 | 37 | ruby_files.each { |ruby_file| require ruby_file } 38 | 39 | # Cap primary key sequences to max(pk). 40 | if connection.respond_to?(:reset_pk_sequence!) 41 | table_names.each do |table_name| 42 | connection.reset_pk_sequence!(table_name) 43 | end 44 | end 45 | end 46 | 47 | return fixtures.size > 1 ? fixtures : fixtures.first 48 | end 49 | end 50 | 51 | def self.destroy_fixtures(table_names) 52 | table_names = [table_names].flatten.map { |n| n.to_s } 53 | connection = ActiveRecord::Base.connection 54 | ActiveRecord::Base.silence do 55 | table_names.each do |table_name| 56 | connection.delete "DELETE FROM #{table_name}", 'Fixture Delete' 57 | end 58 | end 59 | end 60 | 61 | def initialize(connection, table_name, class_name, fixture_paths, file_filter = DEFAULT_FILTER_RE) 62 | @connection, @table_name, @fixture_paths, @file_filter = connection, table_name, fixture_paths, file_filter 63 | @class_name = class_name || 64 | (ActiveRecord::Base.pluralize_table_names ? @table_name.split('.').last.singularize.camelize : @table_name.split('.').last.camelize) 65 | @table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix 66 | read_fixture_files 67 | end 68 | 69 | private 70 | 71 | def read_fixture_files 72 | if yaml_content? 73 | # YAML fixtures 74 | begin 75 | yaml_string = "" 76 | @fixture_paths.each do |fixture_path| 77 | Dir["#{yaml_file_path(fixture_path)}/**/*.yml"].select {|f| test(?f,f) }.each do |subfixture_path| 78 | yaml_string << IO.read(subfixture_path) 79 | end 80 | yaml_string << IO.read(yaml_file_path(fixture_path)) << "\n" 81 | end 82 | 83 | if yaml = YAML::load(erb_render(yaml_string)) 84 | yaml = yaml.value if yaml.respond_to?(:type_id) and yaml.respond_to?(:value) 85 | yaml.each do |name, data| 86 | self[name] = Fixture.new(data, @class_name) 87 | end 88 | end 89 | rescue Exception=>boom 90 | raise Fixture::FormatError, "a YAML error occurred parsing one of #{(@fixture_paths.map { |o| yaml_file_path(o) }).inspect}. 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}" 91 | end 92 | elsif csv_content? 93 | # CSV fixtures 94 | @fixture_paths.each do |fixture_path| 95 | reader = CSV::Reader.create(erb_render(IO.read(csv_file_path(fixture_path)))) 96 | header = reader.shift 97 | i = 0 98 | reader.each do |row| 99 | data = {} 100 | row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip } 101 | self["#{Inflector::underscore(@class_name)}_#{i+=1}"]= Fixture.new(data, @class_name) 102 | end 103 | end 104 | elsif deprecated_yaml_content? 105 | raise Fixture::FormatError, ".yml extension required for all files." 106 | else 107 | # Standard fixtures 108 | Dir.entries(@fixture_path).each do |file| 109 | path = File.join(@fixture_path, file) 110 | if File.file?(path) and file !~ @file_filter 111 | self[file] = Fixture.new(path, @class_name) 112 | end 113 | end 114 | end 115 | end 116 | 117 | def yaml_content? 118 | File.file?(@fixture_paths.first + ".yml") || 119 | File.file?(@fixture_paths.first) 120 | end 121 | 122 | def yaml_file_path(file) 123 | file =~ /\.yml$/ ? file : "#{file}.yml" 124 | end 125 | 126 | def deprecated_yaml_content? 127 | File.file?(@fixture_paths.first + ".yaml") || 128 | File.file?(@fixture_paths.first) 129 | end 130 | 131 | def deprecated_yaml_file_path 132 | "#{@fixture_path}.yaml" 133 | end 134 | 135 | def csv_content? 136 | File.file?(@fixture_paths.first + ".csv") || 137 | File.file?(@fixture_paths.first) 138 | end 139 | 140 | def csv_file_path(file) 141 | file =~ /\.csv$/ ? file : "#{file}.csv" 142 | end 143 | end 144 | 145 | module Test 146 | module Unit 147 | class TestSuite 148 | 149 | def run_with_finish(result, &progress_block) 150 | run_without_finish(result, &progress_block) 151 | name.constantize.finish rescue nil 152 | end 153 | 154 | alias run_without_finish run 155 | alias run run_with_finish 156 | 157 | end 158 | end 159 | end 160 | 161 | module Test #:nodoc: 162 | module Unit #:nodoc: 163 | class TestCase #:nodoc: 164 | class_inheritable_accessor :fixture_file_names 165 | class_inheritable_accessor :ruby_file_names 166 | 167 | class_inheritable_accessor :scenarios_load_root_fixtures 168 | 169 | self.ruby_file_names = [] 170 | self.fixture_file_names = {} 171 | 172 | self.scenarios_load_root_fixtures = true 173 | 174 | def self.finish 175 | Fixtures.destroy_fixtures(fixture_table_names) 176 | end 177 | 178 | def self.fixtures(*table_names) 179 | table_names = table_names.flatten.map { |n| n.to_s } 180 | 181 | table_names.each do |table_name| 182 | self.fixture_file_names[table_name] ||= [] 183 | self.fixture_file_names[table_name] << "#{self.fixture_path}#{table_name}.yml" 184 | end 185 | 186 | self.fixture_table_names |= table_names 187 | require_fixture_classes(table_names) 188 | setup_fixture_accessors(table_names) 189 | end 190 | 191 | def self.scenario(scenario_name = nil, options = {}) 192 | # handle options 193 | defaults = {:root => self.scenarios_load_root_fixtures} 194 | options = defaults.merge(options) 195 | 196 | # find the scenario directory 197 | scenario_path = Dir.glob("#{self.fixture_path}**/*").grep(Regexp.new("/#{scenario_name}$")).first 198 | scenario_path = scenario_path[self.fixture_path.length..scenario_path.length] 199 | scenario_dirs = scenario_path.split('/').unshift('') 200 | 201 | # collect the file paths from which to load 202 | scenario_paths = [] 203 | while !scenario_dirs.empty? 204 | unless !options[:root] && scenario_dirs.size == 1 205 | scenario_paths << self.fixture_path.chop + scenario_dirs.join('/') 206 | end 207 | scenario_dirs.pop 208 | end 209 | scenario_paths.reverse! 210 | 211 | # collect the list of yaml and ruby files 212 | yaml_files = [] 213 | ruby_files = [] 214 | scenario_paths.each do |path| 215 | yaml_files |= Dir.glob("#{path}/*.y{am,m}l") 216 | ruby_files |= Dir.glob("#{path}/*.rb") 217 | end 218 | 219 | # collect table names 220 | table_names = [] 221 | yaml_files.each do |file_path| 222 | file_name = file_path.split("/").last 223 | table_name = file_name[0..file_name.rindex('.') - 1] 224 | table_names << table_name 225 | self.fixture_file_names[table_name] ||= [] 226 | self.fixture_file_names[table_name] << file_path 227 | end 228 | 229 | # collect ruby files 230 | self.ruby_file_names |= ruby_files 231 | 232 | self.fixture_table_names |= table_names 233 | 234 | require_fixture_classes(table_names) 235 | setup_fixture_accessors(table_names) 236 | end 237 | 238 | def self.setup_fixture_accessors(table_names=nil) 239 | (table_names || fixture_table_names).each do |table_name| 240 | table_name = table_name.split('.').last 241 | define_method(table_name) do |fixture, *optionals| 242 | force_reload = optionals.shift 243 | @fixture_cache[table_name] ||= Hash.new 244 | @fixture_cache[table_name][fixture] = nil if force_reload 245 | if @loaded_fixtures[table_name][fixture.to_s] 246 | @fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find 247 | else 248 | raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'" 249 | end 250 | end 251 | end 252 | end 253 | 254 | private 255 | def load_fixtures 256 | @loaded_fixtures = {} 257 | fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_file_names, ruby_file_names, fixture_class_names) 258 | unless fixtures.nil? 259 | if fixtures.instance_of?(Fixtures) 260 | @loaded_fixtures[fixtures.table_name.split('.').last] = fixtures 261 | else 262 | fixtures.each { |f| @loaded_fixtures[f.table_name.split('.').last] = f } 263 | end 264 | end 265 | end 266 | end 267 | end 268 | end --------------------------------------------------------------------------------