edavis10-object_daddy 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,326 @@
1
+ Object Daddy
2
+ ============
3
+ _Version 0.4.3 (February 5, 2010)_
4
+
5
+ __Authors:__ [Rick Bradley](mailto:blogicx@rickbradley.com), [Yossef Mendelssohn](mailto:ymendel@pobox.com)
6
+
7
+ __Copyright:__ Copyright (c) 2007, Flawed Logic, OG Consulting, Rick Bradley, Yossef Mendelssohn
8
+
9
+ __License:__ MIT License. See MIT-LICENSE file for more details.
10
+
11
+ Object Daddy is a library (as well as a Ruby on Rails plugin) designed to
12
+ assist in automating testing of large collections of objects, especially webs
13
+ of ActiveRecord models. It is a descendent of the "Object Mother" pattern for
14
+ creating objects for testing, and is related to the concept of an "object
15
+ exemplar" or _stereotype_.
16
+
17
+ **WARNING** This code is very much at an _alpha_ development stage. Usage, APIs,
18
+ etc., are all subject to change.
19
+
20
+ See [http://b.logi.cx/2007/11/26/object-daddy](http://b.logi.cx/2007/11/26/object-daddy) for inspiration, historical drama, and too much reading.
21
+
22
+ ## Installation
23
+
24
+
25
+ ## As Gem
26
+
27
+ sudo gem install object_daddy
28
+
29
+ config/enviroments/test.rb
30
+
31
+ gem.config "object_daddy"
32
+
33
+
34
+ ## As Plugin
35
+
36
+ Presuming your version of Rails has git plugin installation support:
37
+
38
+ script/plugin install git://github.com/flogic/object_daddy.git
39
+
40
+ Otherwise, you can install object_daddy by hand:
41
+
42
+ 1. Unpack the object_daddy directory into vendor/plugins/ in your rails project.
43
+ 2. Run the object_daddy/install.rb Ruby script.
44
+
45
+
46
+ ## Testing
47
+
48
+ Install the rspec gem and cd into the object_daddy directory. Type `spec
49
+ spec/` and you should see all specs run successfully. If you have autotest
50
+ from the ZenTest gem installed you can run autotest in that directory.
51
+
52
+ ## Using Object Daddy
53
+
54
+
55
+ Object Daddy adds a `.generate` method to every ActiveRecord model which can be
56
+ called to generate a valid instance object of that model class, for use in
57
+ testing:
58
+
59
+ it "should have a comment for every forum the user posts to" do
60
+ @user = User.generate
61
+ @post = Post.generate
62
+ @post.comments << Comment.generate
63
+ @user.should have(1).comments
64
+ end
65
+
66
+ This allows us to generate custom model objects without relying on fixtures,
67
+ and without knowing, in our various widespread tests and specs, the details of
68
+ creating a User, Post, Comment, etc. Not having to know this information means
69
+ the information isn't coded into dozens (or hundreds) of tests, and won't need
70
+ to be changed when the User (Post, Comment, ...) model is refactored later.
71
+
72
+ Object Daddy will identify associated classes that need to be instantiated to
73
+ make the main model valid. E.g., given the following models:
74
+
75
+ class User < ActiveRecord::Base
76
+ belongs_to :login
77
+ validates_presence_of :login
78
+ end
79
+
80
+ class Login < ActiveRecord::Base
81
+ has_one :user
82
+ end
83
+
84
+ A call to `User.generate` will also make a call to `Login.generate` so that
85
+ `User#login` is present, and therefore valid.
86
+
87
+ If all models were able to be created in a valid form by the default Model.new
88
+ call with no knowledge of the model itself, there'd be no need for Object
89
+ Daddy. So, when we deal with models which have validity requirements,
90
+ requiring fields which have format constraints, we need a means of expressing
91
+ how to create those models -- how to satisfy those validity constraints.
92
+
93
+ Object Daddy provides a `generator_for` method which allows the developer to
94
+ specify, for a specific model attribute, how to make a valid value. Note that
95
+ `validates_uniqueness_of` can require that, even if we make 100,000 instances
96
+ of a model that unique attributes cannot have the same values.
97
+
98
+ Object Daddy's `generator_for` method can take three main forms corresponding to
99
+ the means of finding a value for the associated attribute: a block, a method
100
+ call, or using a generator class.
101
+
102
+ class User < ActiveRecord::Base
103
+ validates_presence_of :email
104
+ validates_uniqueness_of :email
105
+ validates_format_of :email,
106
+ :with => /^[-a-z_+0-9.]+@(?:[-a-z_+0-9.]\.)+[a-z]+$/i
107
+ validates_presence_of :username
108
+ validates_format_of :username, :with => /^[a-z0-9_]{4,12}$/i
109
+
110
+ generator_for :email, :start => 'test@domain.com' do |prev|
111
+ user, domain = prev.split('@')
112
+ user.succ + '@' + domain
113
+ end
114
+
115
+ generator_for :username, :method => :next_user
116
+
117
+ generator_for :ssn, :class => SSNGenerator
118
+
119
+ def self.next_user
120
+ @last_username ||= 'testuser'
121
+ @last_username.succ
122
+ end
123
+ end
124
+
125
+ class SSNGenerator
126
+ def self.next
127
+ @last ||= '000-00-0000'
128
+ @last = ("%09d" % (@last.gsub('-', '').to_i + 1)).sub(/^(\d{3})(\d{2})(\d{4})$/, '\1-\2-\3')
129
+ end
130
+ end
131
+
132
+ Note that the block method of invocation (as used with _:email_ above) takes an
133
+ optional _:start_ argument, to specify the value of that attribute on the first
134
+ run. The block will be called thereafter with the previous value of the
135
+ attribute and will generate the next attribute value to be used.
136
+
137
+ A simple default block is provided for any generator with a :start value.
138
+
139
+ class User < ActiveRecord::Base
140
+ generator_for :name, :start => 'Joe' do |prev|
141
+ prev.succ
142
+ end
143
+
144
+ generator_for :name, :start => 'Joe' # equivalent to the above
145
+ end
146
+
147
+ The _:method_ form takes a symbol naming a class method in the model class to be
148
+ called to generate a new value for the attribute in question. If the method
149
+ takes a single argument, it will act much like the block method of invocation,
150
+ being called with the previous value and generating the next.
151
+
152
+ The _:class_ form calls the .next class method on the named class to generate a
153
+ new value for the attribute in question.
154
+
155
+ The argument (previous value) to the block invocation form can be omitted if
156
+ it's going to be ignored, and simple invocation forms are provided for literal
157
+ values.
158
+
159
+ class User < ActiveRecord::Base
160
+ generator_for(:start_time) { Time.now }
161
+ generator_for :name, 'Joe'
162
+ generator_for :age => 25
163
+ end
164
+
165
+ The developer would then simply call `User.generate` when testing.
166
+
167
+ If some attribute values are known (or are being controlled during testing)
168
+ then these can simply be passed in to `.generate`:
169
+
170
+ @bad_login = Login.generate(:expiry => 1.week.ago)
171
+ @expired_user = User.generate(:login => @bad_login)
172
+
173
+ A `.generate!` method is also provided. The _generate/generate!_ pair of methods
174
+ can be thought of as analogs to create/create!, one merely providing an instance
175
+ that may or may not be valid and the other raising an exception if any
176
+ problem comes up.
177
+
178
+ Finally, a `.spawn` method is provided that only gives a new, unsaved object. Note
179
+ that this is the only method of the three that is available if you happen to be
180
+ using Object Daddy outside of Rails.
181
+
182
+ ## Exemplars
183
+
184
+ In the examples given above we are using `generator_for` in the bodies of the
185
+ models themselves. Given that Object Daddy is primarily geared towards
186
+ annotating models with information useful for testing, we anticipate that
187
+ `generator_for` should not normally be included inline in models. Rather, we
188
+ will provide a place where model classes can be re-opened and `generator_for`
189
+ calls (and support methods) can be written without polluting the model files
190
+ with Object Daddy information.
191
+
192
+ Object Daddy, when installed as a Rails plugin, will create
193
+ *RAILS_ROOT/spec/exemplars/* as a place to hold __exemplar__ files for Rails model
194
+ classes. (We are seeking perhaps some better terminology)
195
+
196
+ An __exemplar__ for the User model would then be found in
197
+ *RAILS_ROOT/spec/exemplars/user_exemplar.rb* (when you are using a testing tool
198
+ which works from *RAILS_ROOT/test*, Object Daddy will create
199
+ *RAILS_ROOT/test/exemplars* and look for your exemplars in that directory
200
+ instead). Exemplar files are completely optional, and no model need have
201
+ exemplar files. The `.generate` method will still exist and be callable, and
202
+ `generator_for` can be declared in the model files themselves. If an exemplar
203
+ file is available when `.generate` is called on a model, the exemplar file will
204
+ be loaded and used. An example *user_exemplar.rb* appears below:
205
+
206
+ require 'ssn_generator'
207
+
208
+ class User < ActiveRecord::Base
209
+ generator_for :email, :start => 'test@domain.com' do |prev|
210
+ user, domain = prev.split('@')
211
+ user.succ + '@' + domain
212
+ end
213
+
214
+ generator_for :username, :method => :next_user
215
+
216
+ generator_for :ssn, :class => SSNGenerator
217
+
218
+ def self.next_user
219
+ @last_username ||= 'testuser'
220
+ @last_username.succ
221
+ end
222
+ end
223
+
224
+ ## Blocks
225
+
226
+ The `spawn`, `generate` and `generate!` methods can all accept a block, to which
227
+ they'll yield the generated object. This provides a nice scoping mechanism in
228
+ your code examples. Consider:
229
+
230
+ describe "admin user" do
231
+ it "should be authorized to create company profiles"
232
+ admin_user = User.generate!
233
+ admin_user.activate!
234
+ admin_user.add_role("admin")
235
+
236
+ admin_user.should be_authorized(:create, Company)
237
+ end
238
+ end
239
+
240
+ This could be refactored to:
241
+
242
+ describe "admin user" do
243
+ it "should be authorized to create company profiles" do
244
+ admin_user = User.generate! do |user|
245
+ user.activate!
246
+ user.add_role("admin")
247
+ end
248
+
249
+ admin_user.should be_authorized(:create, Company)
250
+ end
251
+ end
252
+
253
+ Or:
254
+
255
+ describe "admin user" do
256
+ it "should be authorized to create company profiles"
257
+ User.generate! do |user|
258
+ user.activate!
259
+ user.add_role("admin")
260
+ end.should be_authorized(:create, Company)
261
+ end
262
+ end
263
+
264
+ Or even:
265
+
266
+ describe "admin user" do
267
+ def admin_user
268
+ @admin_user ||= User.generate! do |user|
269
+ user.activate!
270
+ user.add_role("admin")
271
+ end
272
+ end
273
+
274
+ it "should be authorized to create company profiles"
275
+ admin_user.should be_authorized(:create, Company)
276
+ end
277
+ end
278
+
279
+ This last refactoring allows you to reuse the admin_user method across
280
+ multiple code examples, balancing DRY with local data.
281
+
282
+ ## Object Daddy and Fixtures
283
+
284
+ While Object Daddy is meant to obviate the hellish devilspawn that are test
285
+ fixtures, Object Daddy should work alongside fixtures just fine. To each his
286
+ own, I suppose.
287
+
288
+ ## Known Issues
289
+
290
+ The simple invocation forms for `generator_for` when using literal values do not
291
+ work if the literal value is a Hash. Don't do that.
292
+
293
+ class User < ActiveRecord::Base
294
+ generator_for :thing_hash, { 'some key' => 'some value' }
295
+ generator_for :other_hash => { 'other key' => 'other value' }
296
+ end
297
+
298
+ I'm not sure why this would even ever come up, but seriously, don't.
299
+
300
+ Required `belongs_to` associations are automatically generated when generating an instance,
301
+ but only if necessary.
302
+
303
+ class Category < ActiveRecord::Base
304
+ has_many :items
305
+ end
306
+
307
+ class Item < ActiveRecord::Base
308
+ belongs_to :category
309
+ validates_presence_of :category
310
+ end
311
+
312
+ `Item.generate` will generate a new category, but `some_category.items.generate` will not.
313
+ Unless, of course, you are foolish enough to define a generator in the exemplar.
314
+
315
+ class Item
316
+ generator_for(:category) { Category.generate }
317
+ end
318
+
319
+ Once again, don't do that.
320
+
321
+ ## Rails _surprises_
322
+
323
+ Due to the way Rails handles associations, cascading generations (as a result of
324
+ required associations) are always generated-and-saved, even if the original generation
325
+ call was a mere `spawn` (`new`). This may come as a surprise, but it would probably be more
326
+ of a surprise if `User.spawn.save` and `User.generate` weren't comparable.
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 4
4
+ :patch: 3
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
@@ -0,0 +1,28 @@
1
+ require 'fileutils'
2
+
3
+ def readme_contents
4
+ IO.read(File.join(File.dirname(__FILE__), 'README.markdown'))
5
+ end
6
+
7
+ rails_root = File.dirname(__FILE__) + '/../../../'
8
+
9
+ if File.directory?(rails_root + 'spec')
10
+ unless File.directory?(rails_root + 'spec/exemplars')
11
+ puts "Creating directory [#{rails_root + 'spec/exemplars'}]"
12
+ FileUtils.mkdir(rails_root + 'spec/exemplars')
13
+ end
14
+ else
15
+ if File.directory?(rails_root + 'test')
16
+ unless File.directory?(rails_root + 'test/exemplars')
17
+ puts "Creating directory [#{rails_root + 'test/exemplars'}]"
18
+ FileUtils.mkdir(rails_root + 'test/exemplars')
19
+ end
20
+ else
21
+ puts "Creating directory [#{rails_root + 'spec'}]"
22
+ FileUtils.mkdir(rails_root + 'spec')
23
+ puts "Creating directory [#{rails_root + 'spec/exemplars'}]"
24
+ FileUtils.mkdir(rails_root + 'spec/exemplars')
25
+ end
26
+ end
27
+
28
+ puts readme_contents
@@ -0,0 +1,254 @@
1
+ module ObjectDaddy
2
+
3
+ def self.included(klass)
4
+ klass.extend ClassMethods
5
+ if defined? ActiveRecord and klass < ActiveRecord::Base
6
+ klass.extend RailsClassMethods
7
+
8
+ class << klass
9
+ alias_method :validates_presence_of_without_object_daddy, :validates_presence_of
10
+ alias_method :validates_presence_of, :validates_presence_of_with_object_daddy
11
+ end
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ attr_accessor :exemplars_generated, :exemplar_path, :generators
17
+ attr_reader :presence_validated_attributes
18
+ protected :exemplars_generated=
19
+
20
+ # :call-seq:
21
+ # spawn()
22
+ # spawn() do |obj| ... end
23
+ # spawn(args)
24
+ # spawn(args) do |obj| ... end
25
+ #
26
+ # Creates a valid instance of this class, using any known generators. The
27
+ # generated instance is yielded to a block if provided.
28
+ def spawn(args = {})
29
+ gather_exemplars
30
+ if @concrete_subclass_name
31
+ return block_given? \
32
+ ? const_get(@concrete_subclass_name).spawn(args) {|instance| yield instance} \
33
+ : const_get(@concrete_subclass_name).spawn(args)
34
+ end
35
+ generate_values(args)
36
+ instance = new
37
+ args.each_pair do |attribute, value|
38
+ instance.send("#{attribute}=", value) # support setting of mass-assignment protected attributes
39
+ end
40
+ yield instance if block_given?
41
+ instance
42
+ end
43
+
44
+ # register a generator for an attribute of this class
45
+ # generator_for :foo do |prev| ... end
46
+ # generator_for :foo do ... end
47
+ # generator_for :foo, value
48
+ # generator_for :foo => value
49
+ # generator_for :foo, :class => GeneratorClass
50
+ # generator_for :foo, :method => :method_name
51
+ def generator_for(handle, args = {}, &block)
52
+ if handle.is_a?(Hash)
53
+ raise ArgumentError, "only specify one attr => value pair at a time" unless handle.keys.length == 1
54
+ gen_data = handle
55
+ handle = gen_data.keys.first
56
+ args = gen_data[handle]
57
+ end
58
+
59
+ raise ArgumentError, "an attribute name must be specified" unless handle = handle.to_sym
60
+
61
+ unless args.is_a?(Hash)
62
+ unless block
63
+ retval = args
64
+ block = lambda { retval } # lambda { args } results in returning the empty hash that args gets changed to
65
+ end
66
+ args = {} # args is assumed to be a hash for the rest of the method
67
+ end
68
+
69
+ if args[:start]
70
+ block ||= lambda { |prev| prev.succ }
71
+ end
72
+
73
+ if args[:method]
74
+ h = { :method => args[:method].to_sym }
75
+ h[:start] = args[:start] if args[:start]
76
+ record_generator_for(handle, h)
77
+ elsif args[:class]
78
+ raise ArgumentError, "generator class [#{args[:class].name}] does not have a :next method" unless args[:class].respond_to?(:next)
79
+ record_generator_for(handle, :class => args[:class])
80
+ elsif block
81
+ raise ArgumentError, "generator block must take an optional single argument" unless (-1..1).include?(block.arity) # NOTE: lambda {} has an arity of -1, while lambda {||} has an arity of 0
82
+ h = { :block => block }
83
+ h[:start] = args[:start] if args[:start]
84
+ record_generator_for(handle, h)
85
+ else
86
+ raise ArgumentError, "a block, :class generator, :method generator, or value must be specified to generator_for"
87
+ end
88
+ end
89
+
90
+ def generates_subclass(subclass_name)
91
+ @concrete_subclass_name = subclass_name.to_s
92
+ end
93
+
94
+ def gather_exemplars
95
+ return if exemplars_generated
96
+
97
+ self.generators ||= {}
98
+ if superclass.respond_to?(:gather_exemplars)
99
+ superclass.gather_exemplars
100
+ self.generators = (superclass.generators).merge(generators).dup
101
+ end
102
+
103
+ exemplar_path.each do |raw_path|
104
+ path = File.join(raw_path, "#{underscore(name)}_exemplar.rb")
105
+ load(path) if File.exists?(path)
106
+ end
107
+
108
+ self.exemplars_generated = true
109
+ end
110
+
111
+ def presence_validated_attributes
112
+ @presence_validated_attributes ||= {}
113
+ attrs = @presence_validated_attributes
114
+ if superclass.respond_to?(:presence_validated_attributes)
115
+ attrs = superclass.presence_validated_attributes.merge(attrs)
116
+ end
117
+ attrs
118
+ end
119
+
120
+ protected
121
+
122
+ # we define an underscore helper ourselves since the ActiveSupport isn't available if we're not using Rails
123
+ def underscore(string)
124
+ string.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
125
+ end
126
+
127
+ def record_generator_for(handle, generator)
128
+ self.generators ||= {}
129
+ raise ArgumentError, "a generator for attribute [:#{handle}] has already been specified" if (generators[handle] || {})[:source] == self
130
+ generators[handle] = { :generator => generator, :source => self }
131
+ end
132
+
133
+ private
134
+
135
+ def generate_values(args)
136
+ (generators || {}).each_pair do |handle, gen_data|
137
+ next if args.include?(handle) or args.include?(handle.to_s)
138
+
139
+ generator = gen_data[:generator]
140
+ if generator[:block]
141
+ process_generated_value(args, handle, generator, generator[:block])
142
+ elsif generator[:method]
143
+ method = method(generator[:method])
144
+ if method.arity == 1
145
+ process_generated_value(args, handle, generator, method)
146
+ else
147
+ args[handle] = method.call
148
+ end
149
+ elsif generator[:class]
150
+ args[handle] = generator[:class].next
151
+ end
152
+ end
153
+
154
+ generate_missing(args)
155
+ end
156
+
157
+ def process_generated_value(args, handle, generator, block)
158
+ if generator[:start]
159
+ value = generator[:start]
160
+ generator.delete(:start)
161
+ else
162
+ if block.arity == 0
163
+ value = block.call
164
+ else
165
+ value = block.call(generator[:prev])
166
+ end
167
+ end
168
+ generator[:prev] = args[handle] = value
169
+ end
170
+
171
+ def generate_missing(args)
172
+ if presence_validated_attributes and !presence_validated_attributes.empty?
173
+ req = {}
174
+ (presence_validated_attributes.keys - args.keys).each {|a| req[a.to_s] = true } # find attributes required by validates_presence_of not already set
175
+
176
+ belongs_to_associations = reflect_on_all_associations(:belongs_to).to_a
177
+ missing = belongs_to_associations.select { |a| req[a.name.to_s] or req[a.primary_key_name.to_s] }
178
+ if create_scope = scope(:create)
179
+ missing.reject! { |a| create_scope.include?(a.primary_key_name) }
180
+ end
181
+ missing.reject! { |a| [a.name, a.primary_key_name].any? { |n| args.stringify_keys.include?(n.to_s) } }
182
+ missing.each {|a| args[a.name] = a.class_name.constantize.generate }
183
+ end
184
+ end
185
+ end
186
+
187
+ module RailsClassMethods
188
+ def exemplar_path
189
+ paths = ['spec', 'test'].inject([]) do |array, dir|
190
+ if File.directory?(File.join(RAILS_ROOT, dir))
191
+ array << File.join(RAILS_ROOT, dir, 'exemplars')
192
+ end
193
+ array
194
+ end
195
+ end
196
+
197
+ def validates_presence_of_with_object_daddy(*attr_names)
198
+ @presence_validated_attributes ||= {}
199
+ new_attr = attr_names.dup
200
+ new_attr.pop if new_attr.last.is_a?(Hash)
201
+ new_attr.each {|a| @presence_validated_attributes[a] = true }
202
+ validates_presence_of_without_object_daddy(*attr_names)
203
+ end
204
+
205
+ # :call-seq:
206
+ # generate()
207
+ # generate() do |obj| ... end
208
+ # generate(args)
209
+ # generate(args) do |obj| ... end
210
+ #
211
+ # Creates and tries to save an instance of this class, using any known
212
+ # generators. The generated instance is yielded to a block if provided.
213
+ #
214
+ # This will not raise errors on a failed save. Use generate! if you
215
+ # want errors raised.
216
+ def generate(args = {})
217
+ spawn(args) do |instance|
218
+ instance.save
219
+ yield instance if block_given?
220
+ end
221
+ end
222
+
223
+ # :call-seq:
224
+ # generate()
225
+ # generate() do |obj| ... end
226
+ # generate(args)
227
+ # generate(args) do |obj| ... end
228
+ #
229
+ # Creates and tries to save! an instance of this class, using any known
230
+ # generators. The generated instance is yielded to a block if provided.
231
+ #
232
+ # This will raise errors on a failed save. Use generate if you do not want
233
+ # errors raised.
234
+ def generate!(args = {})
235
+ spawn(args) do |instance|
236
+ instance.save!
237
+ yield instance if block_given?
238
+ end
239
+ end
240
+ end
241
+ end
242
+
243
+ unless ActiveRecord::Base.respond_to? :inherited_with_object_daddy
244
+ class ActiveRecord::Base
245
+ def self.inherited_with_object_daddy(subclass)
246
+ self.inherited_without_object_daddy(subclass)
247
+ subclass.send(:include, ObjectDaddy) unless subclass < ObjectDaddy
248
+ end
249
+
250
+ class << self
251
+ alias_method_chain :inherited, :object_daddy
252
+ end
253
+ end
254
+ end