myronmarston-factory_data_preloader 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Myron Marston, Kashless.org
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.
data/README.rdoc ADDED
@@ -0,0 +1,80 @@
1
+ = factory_data_preloader
2
+
3
+ If you're like me, you really dislike using rails test fixtures. On the rails projects I've worked on that
4
+ have used test fixtures, we've had a number of tests that passed as false positives because the test
5
+ fixtures don't represent "real data". Fixtures just dump the data you've defined directly into the database,
6
+ without going through the model--meaning you don't get any of the validations or before/after save callback
7
+ behavior defined on your models.
8
+
9
+ There are multiple gems and plugins that address this; my personal favorite is factory girl.
10
+
11
+ However, none of the solutions I've tried are as fast as using test fixtures. When you use test fixtures,
12
+ rails rolls back the database transaction used in each test and allows you to re-use your fixture data.
13
+ This is much much quicker then creating the data you will use directly in each test.
14
+
15
+ This gem attempts to solve this issue, and give you the best of both worlds: create your test data using
16
+ factory girl, the models themselves, or any other solution you want, while also being able to pre-load it
17
+ and re-use it in each test when rails rolls back the transaction.
18
+
19
+ == Download
20
+
21
+ Github: http://github.com/myronmarston/factory_data_preloader/tree/master
22
+
23
+ Gem:
24
+ gem install myronmarston-factory_data_preloader --source http://gems.github.com
25
+
26
+ == Usage
27
+
28
+ Load the gem using Rails' 2.1+ gem support, in either config/environment.rb, or config/environments/test.rb:
29
+ config.gem 'myronmarston-factory_data_preloader',
30
+ :lib => 'factory_data_preloader',
31
+ :source => 'http://gems.github.com'
32
+
33
+ Define your preloaded data. Simply require the file(s) that define the preloaded data in your test/test_helper.rb.
34
+ Define your data like this:
35
+
36
+ FactoryData.preload(:users) do |data|
37
+ data[:thom] = User.create(:first_name => 'Thom', :last_name => 'York')
38
+ data[:john] = User.create(:first_name => 'John', :last_name => 'Doe')
39
+ end
40
+
41
+ FactoryData.preload(:posts) do |data|
42
+ data[:tour] = FactoryData.users(:thom).posts.create(:title => 'Tour!', :body => 'Radiohead will tour soon.')
43
+ end
44
+
45
+ FactoryData.preload(:some_other_posts, :model_class => Post) do |data|
46
+ # note the use of the :model_class option when the model class cannot be inferred from the symbol.
47
+ data[:another_post] = Post.create(:user => FactoryData.users(:john), :title => 'Life is good')
48
+ end
49
+
50
+ FactoryData.preload(:comments) do |data|
51
+ data[:woohoo] = FactoryData.users(:john).comments.create(:post => FactoryData.posts(:tour), :comment => "I can't wait!")
52
+ end
53
+
54
+ Finally, use this preloaded data in your tests:
55
+
56
+ class UserTest < ActiveSupport::TestCase
57
+ def test_thom_has_last_name
58
+ user = FactoryData.users(:thom)
59
+ assert_equal 'York', user.last_name
60
+ end
61
+ end
62
+
63
+ == Notes, etc.
64
+
65
+ * This gem has been tested with Rails 2.2.2 and Rails 2.3.2.
66
+ * You can create the data using any gem or plugin you want. In this contrived example, I just used ActiveRecord's
67
+ built in methods for simplicity's sake.
68
+ * FactoryData#preload does not actually preload the data. It simply defines the data that will be automatically preloaded
69
+ at the appropriate time (namely, at the same time when rails loads the fixtures).
70
+ * FactoryData#preload defines a new method on FactoryData using the same name as the symbol.
71
+ * This can be mixed-n-matched with fixtures. You may want to keep using fixtures in an existing code base,
72
+ or migrate over to this slowly.
73
+ * The data gets preloaded in the order the preloaders are defined. Note in the example above that the users defined in
74
+ the first preloader get used in the later preloaders.
75
+ * FactoryData#preload attempts to infer the appropriate model class from the symbol you pass. If your symbol doesn't
76
+ match the model class, pass the model class using the :model_class option.
77
+
78
+ == Copyright
79
+
80
+ Copyright (c) 2009 Myron Marston, Kashless.org. See LICENSE for details.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
@@ -0,0 +1,23 @@
1
+ # http://github.com/rails/rails/tree/823b623fe2de8846c37aa13250010809ac940b57/activesupport/lib/active_support/core_ext/object/misc.rb
2
+
3
+ unless Object.respond_to?(:try) # Object#try is in Rails 2.3 but not in 2.2.
4
+ class Object
5
+ # Tries to send the method only if object responds to it. Return +nil+ otherwise.
6
+ # It will also forward any arguments and/or block like Object#send does.
7
+ #
8
+ # ==== Example :
9
+ #
10
+ # # Without try
11
+ # @person ? @person.name : nil
12
+ #
13
+ # With try
14
+ # @person.try(:name)
15
+ #
16
+ # # try also accepts arguments/blocks for the method it is trying
17
+ # Person.try(:find, 1)
18
+ # @people.try(:map) {|p| p.name}
19
+ def try(method, *args, &block)
20
+ send(method, *args, &block) if respond_to?(method, true)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,84 @@
1
+ require 'ostruct'
2
+
3
+ class PreloaderAlreadyDefinedError < StandardError; end
4
+ class PreloadedRecordNotFound < StandardError; end
5
+
6
+ class FactoryData
7
+ @@preloaded_cache = nil
8
+ @@preloaded_data_deleted = nil
9
+ @@single_test_cache = {}
10
+ @@preloaders = []
11
+
12
+ class << self
13
+
14
+ def preload(model_type, options = {}, &block)
15
+ raise PreloaderAlreadyDefinedError.new, "You have already defined the preloader for #{model_type.to_s}" if @@preloaders.map(&:model_type).include?(model_type)
16
+
17
+ model_class = options[:model_class] || model_type.to_s.singularize.classify.constantize
18
+ @@preloaders << OpenStruct.new(:model_type => model_type, :model_class => model_class, :proc => block)
19
+
20
+ class << self; self; end.class_eval do
21
+ define_method model_type do |key|
22
+ get_record(model_type, model_class, key)
23
+ end
24
+ end
25
+ end
26
+
27
+ def delete_preload_data!
28
+ # make sure this only runs once...
29
+ return unless @@preloaded_data_deleted.nil?
30
+
31
+ # the preloaders are listed in the parent -> child table order,
32
+ # so we need to delete them in reverse.
33
+ @@preloaders.reverse.each do |preloader|
34
+ preloader.model_class.delete_all
35
+ end
36
+
37
+ @@preloaded_data_deleted = true
38
+ end
39
+
40
+ def preload_data!
41
+ return unless @@preloaded_cache.nil? # make sure the data is only preloaded once.
42
+ @@preloaded_cache = {}
43
+
44
+ @@preloaders.each do |preloader|
45
+ cache = @@preloaded_cache[preloader.model_type] ||= {}
46
+ data = {}
47
+ preloader.proc.try(:call, data)
48
+ data.each do |key, record|
49
+ if record.new_record? && !record.save
50
+ puts "\nError preloading factory data. #{preloader.model_class.to_s} :#{key.to_s} could not be saved. Errors: "
51
+ puts pretty_error_messages(record)
52
+ puts "\n\n"
53
+ next
54
+ end
55
+
56
+ cache[key] = record.id
57
+ end
58
+ end
59
+ end
60
+
61
+ def reset_cache!
62
+ @@single_test_cache = {}
63
+ end
64
+
65
+ private
66
+
67
+ def get_record(type, model_class, key)
68
+ @@single_test_cache[type] ||= {}
69
+ @@single_test_cache[type][key] ||= begin
70
+ record = model_class.find_by_id(@@preloaded_cache[type][key])
71
+ raise PreloadedRecordNotFound.new, "Could not find a record for FactoryData.#{type}(:#{key})." unless record
72
+ record
73
+ end
74
+ end
75
+
76
+ # Borrowed from shoulda: http://github.com/thoughtbot/shoulda/blob/e02228d45a879ff92cb72b84f5fccc6a5f856a65/lib/shoulda/active_record/helpers.rb#L4-9
77
+ def pretty_error_messages(obj)
78
+ obj.errors.map do |a, m|
79
+ msg = "#{a} #{m}"
80
+ msg << " (#{obj.send(a).inspect})" unless a.to_sym == :base
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,33 @@
1
+ # TODO: add tests for this. I've manually tested this in a Rails 2.2 and 2.3 app, but haven't found a way to add
2
+ # a test for this to our test suite. It's difficult to test this since it just modifies what happens before the
3
+ # tests are run.
4
+
5
+ # Between Rails 2.2 and 2.3, the fixture loading code was moved from
6
+ # Test::Unit::TestCase to ActiveRecord::TestFixtures. See this commit:
7
+ # http://github.com/rails/rails/commit/b0ee1bdf2650d7a8380d4e9be58bba8d9c5bd40e
8
+ patch_module = defined?(ActiveRecord::TestFixtures) ? ActiveRecord::TestFixtures : Test::Unit::TestCase
9
+
10
+ patch_module.class_eval do
11
+ def load_fixtures_with_preloaded_factory_data
12
+ val = load_fixtures_without_preloaded_factory_data
13
+ FactoryData.preload_data!
14
+ val
15
+ end
16
+
17
+ def teardown_fixtures_with_preloaded_factory_data
18
+ FactoryData.reset_cache!
19
+ teardown_fixtures_without_preloaded_factory_data
20
+ end
21
+
22
+ alias_method_chain :load_fixtures, :preloaded_factory_data
23
+ alias_method_chain :teardown_fixtures, :preloaded_factory_data
24
+ end
25
+
26
+ class Fixtures
27
+ def delete_existing_fixtures_with_preloaded_factory_data
28
+ delete_existing_fixtures_without_preloaded_factory_data
29
+ FactoryData.delete_preload_data!
30
+ end
31
+
32
+ alias_method_chain :delete_existing_fixtures, :preloaded_factory_data
33
+ end
@@ -0,0 +1,16 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+
4
+ if ENV['RAILS_VERSION']
5
+ puts "loading Rails version #{ENV['RAILS_VERSION']}"
6
+ gem "activesupport", "= #{ENV['RAILS_VERSION']}"
7
+ gem "activerecord", "= #{ENV['RAILS_VERSION']}"
8
+ end
9
+
10
+ require 'active_support'
11
+ require 'active_record'
12
+ require 'active_record/fixtures'
13
+
14
+ require 'factory_data_preloader/core_ext'
15
+ require 'factory_data_preloader/factory_data'
16
+ require 'factory_data_preloader/rails_core_ext'
@@ -0,0 +1,156 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class FactoryDataTest < Test::Unit::TestCase
4
+ def setup
5
+ FactoryData.reset!
6
+ end
7
+
8
+ context 'Calling FactoryData.preload(:users)' do
9
+ setup do
10
+ FactoryData.preload(:users) do |data|
11
+ data[:thom] = User.create(:first_name => 'Thom', :last_name => 'York')
12
+ end
13
+ end
14
+
15
+ should_not_change 'User.count'
16
+ should_change "FactoryData.methods.include?('users')", :from => false, :to => true
17
+
18
+ should 'not allow it to be called again' do
19
+ assert_raise PreloaderAlreadyDefinedError do
20
+ FactoryData.preload(:users)
21
+ end
22
+ end
23
+
24
+ context 'when there was a previous user record in the database' do
25
+ setup { User.create(:first_name => 'Barack', :last_name => 'Obama') }
26
+
27
+ context 'and calling FactoryData.delete_preload_data!' do
28
+ setup { FactoryData.delete_preload_data! }
29
+ should_change 'User.count', :to => 0
30
+
31
+ context 'and there is another record in the database' do
32
+ setup { User.create(:first_name => 'George', :last_name => 'Bush') }
33
+
34
+ context 'and FactoryData.delete_preload_data! is called again' do
35
+ setup { FactoryData.delete_preload_data! }
36
+ should_not_change 'User.count'
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ context 'and later calling FactoryData.preload_data!' do
43
+ setup { FactoryData.preload_data! }
44
+
45
+ should_change 'User.count', :by => 1
46
+
47
+ context 'and later calling FactoryData.users(key)' do
48
+ setup { @user = FactoryData.users(:thom) }
49
+
50
+ should 'retrieve the correct user' do
51
+ assert_equal 'Thom', @user.first_name
52
+ assert_equal 'York', @user.last_name
53
+ assert !@user.new_record?
54
+ end
55
+
56
+ should 'raise the appropriate error for a non-existant key' do
57
+ assert_raise PreloadedRecordNotFound do
58
+ FactoryData.users(:not_a_user)
59
+ end
60
+ end
61
+
62
+ should 'cache the record so as not to use User.find more than necessary' do
63
+ User.expects(:find).never
64
+ user2 = FactoryData.users(:thom)
65
+ assert_equal @user.object_id, user2.object_id
66
+ end
67
+
68
+ context 'and later calling FactoryData.reset_cache!' do
69
+ setup { FactoryData.reset_cache! }
70
+
71
+ should 'reload the record from the database the next time FactoryData.users(key) is called' do
72
+ User.expects(:find).once.returns(@user)
73
+ FactoryData.users(:thom)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ context 'Preloading a record that has not been saved' do
81
+ setup do
82
+ @unsaved_user = User.new(:first_name => 'George', :last_name => 'Washington')
83
+ assert @unsaved_user.new_record?
84
+
85
+ FactoryData.preload(:users) do |data|
86
+ data[:george] = @unsaved_user
87
+ end
88
+ end
89
+
90
+ should 'save the record wen preload_data! is called' do
91
+ FactoryData.preload_data!
92
+ assert !@unsaved_user.new_record?
93
+ end
94
+ end
95
+
96
+ context 'Preloading a record that cannot be saved to the database' do
97
+ setup do
98
+ @invalid_user = User.new(:first_name => 'Bob')
99
+ assert !@invalid_user.valid?
100
+
101
+ FactoryData.preload(:users) do |data|
102
+ data[:bob] = @invalid_user
103
+ end
104
+ end
105
+
106
+ should 'print an appropriate error message when preload_data! is called' do
107
+ out, err = OutputCapturer.capture do
108
+ FactoryData.preload_data!
109
+ end
110
+
111
+ assert_match /Error preloading factory data\.\s+User :bob could not be saved\.\s+Errors:\s+last_name can't be blank/im, out
112
+ end
113
+ end
114
+
115
+ context 'Preloading with an explicit :model_class option' do
116
+ setup do
117
+ FactoryData.preload(:posts, :model_class => User) do |data|
118
+ data[:george] = User.create(:first_name => 'George', :last_name => 'Washington')
119
+ end
120
+ FactoryData.preload_data!
121
+ end
122
+
123
+ should 'use the passed model_class rather than inferring the class from the symbol' do
124
+ assert_equal User, FactoryData.posts(:george).class
125
+ end
126
+ end
127
+
128
+ context 'Preloading multiple record types' do
129
+ setup do
130
+ FactoryData.preload(:users) do |data|
131
+ data[:thom] = User.create(:first_name => 'Thom', :last_name => 'York')
132
+ data[:john] = User.create(:first_name => 'John', :last_name => 'Doe')
133
+ end
134
+
135
+ FactoryData.preload(:posts) do |data|
136
+ data[:tour] = FactoryData.users(:thom).posts.create(:title => 'Tour!', :body => 'Radiohead will tour soon.')
137
+ end
138
+
139
+ FactoryData.preload(:comments) do |data|
140
+ data[:woohoo] = FactoryData.users(:john).comments.create(:post => FactoryData.posts(:tour), :comment => "I can't wait!")
141
+ end
142
+
143
+ FactoryData.preload_data!
144
+ end
145
+
146
+ should 'preload them in the defined order, allowing you to use prior definitions in later definitions' do
147
+ assert_equal 'Thom', FactoryData.users(:thom).first_name
148
+ assert_equal 'John', FactoryData.users(:john).first_name
149
+
150
+ assert_equal FactoryData.users(:thom), FactoryData.posts(:tour).user
151
+
152
+ assert_equal FactoryData.users(:john), FactoryData.comments(:woohoo).user
153
+ assert_equal FactoryData.posts(:tour), FactoryData.comments(:woohoo).post
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,14 @@
1
+ class User < ActiveRecord::Base
2
+ has_many :posts
3
+ has_many :comments
4
+ validates_presence_of :last_name
5
+ end
6
+
7
+ class Post < ActiveRecord::Base
8
+ belongs_to :user
9
+ end
10
+
11
+ class Comment < ActiveRecord::Base
12
+ belongs_to :post
13
+ belongs_to :user
14
+ end
@@ -0,0 +1,27 @@
1
+ OutputCapturer.capture do
2
+ ActiveRecord::Schema.define do
3
+ create_table :users, :force => true do |t|
4
+ t.string :first_name
5
+ t.string :last_name
6
+ t.timestamps
7
+ end
8
+ end
9
+
10
+ ActiveRecord::Schema.define do
11
+ create_table :posts, :force => true do |t|
12
+ t.references :user
13
+ t.string :title
14
+ t.string :body
15
+ t.timestamps
16
+ end
17
+ end
18
+
19
+ ActiveRecord::Schema.define do
20
+ create_table :comments, :force => true do |t|
21
+ t.references :user
22
+ t.references :post
23
+ t.string :comment
24
+ t.timestamps
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,62 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ begin
6
+ require 'ruby-debug'
7
+ Debugger.start
8
+ Debugger.settings[:autoeval] = true if Debugger.respond_to?(:settings)
9
+ rescue LoadError
10
+ # ruby-debug wasn't available so neither can the debugging be
11
+ end
12
+
13
+
14
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
15
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
16
+
17
+ require 'factory_data_preloader'
18
+
19
+ ActiveRecord::Base.establish_connection({ :database => ":memory:", :adapter => 'sqlite3', :timeout => 500 })
20
+
21
+ module OutputCapturer
22
+ # borrowed from zentest assertions...
23
+ def self.capture
24
+ require 'stringio'
25
+ orig_stdout = $stdout.dup
26
+ orig_stderr = $stderr.dup
27
+ captured_stdout = StringIO.new
28
+ captured_stderr = StringIO.new
29
+ $stdout = captured_stdout
30
+ $stderr = captured_stderr
31
+ yield
32
+ captured_stdout.rewind
33
+ captured_stderr.rewind
34
+ return captured_stdout.string, captured_stderr.string
35
+ ensure
36
+ $stdout = orig_stdout
37
+ $stderr = orig_stderr
38
+ end
39
+ end
40
+
41
+ class FactoryData
42
+ # helper method to reset the factory data between test runs.
43
+ def self.reset!
44
+ @@preloaders.reverse.each do |preloader|
45
+ class << self; self; end.class_eval do
46
+ remove_method(preloader.model_type)
47
+ end
48
+
49
+ unless @@preloaded_cache.nil?
50
+ preloader.model_class.delete_all(:id => @@preloaded_cache[preloader.model_type].values)
51
+ end
52
+ end
53
+
54
+ @@preloaded_cache = nil
55
+ @@preloaded_data_deleted = nil
56
+ @@single_test_cache = {}
57
+ @@preloaders = []
58
+ end
59
+ end
60
+
61
+ require 'lib/schema'
62
+ require 'lib/models'
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: myronmarston-factory_data_preloader
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Myron Marston
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-30 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: myron.marston@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ - LICENSE
25
+ files:
26
+ - README.rdoc
27
+ - VERSION.yml
28
+ - lib/factory_data_preloader
29
+ - lib/factory_data_preloader/core_ext.rb
30
+ - lib/factory_data_preloader/factory_data.rb
31
+ - lib/factory_data_preloader/rails_core_ext.rb
32
+ - lib/factory_data_preloader.rb
33
+ - test/factory_data_test.rb
34
+ - test/lib
35
+ - test/lib/models.rb
36
+ - test/lib/schema.rb
37
+ - test/test_helper.rb
38
+ - LICENSE
39
+ has_rdoc: true
40
+ homepage: http://github.com/myronmarston/factory_data_preloader
41
+ post_install_message:
42
+ rdoc_options:
43
+ - --inline-source
44
+ - --charset=UTF-8
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.2.0
63
+ signing_key:
64
+ specification_version: 2
65
+ summary: A library for preloading test data in rails applications.
66
+ test_files: []
67
+