object_daddy 0.4.1

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/README.markdown ADDED
@@ -0,0 +1,326 @@
1
+ Object Daddy
2
+ ============
3
+ _Version 0.4.1 (April 28, 2009)_
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.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 4
4
+ :patch: 1
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
data/install.rb ADDED
@@ -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,239 @@
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(args)
37
+ yield instance if block_given?
38
+ instance
39
+ end
40
+
41
+ # register a generator for an attribute of this class
42
+ # generator_for :foo do |prev| ... end
43
+ # generator_for :foo do ... end
44
+ # generator_for :foo, value
45
+ # generator_for :foo => value
46
+ # generator_for :foo, :class => GeneratorClass
47
+ # generator_for :foo, :method => :method_name
48
+ def generator_for(handle, args = {}, &block)
49
+ if handle.is_a?(Hash)
50
+ raise ArgumentError, "only specify one attr => value pair at a time" unless handle.keys.length == 1
51
+ gen_data = handle
52
+ handle = gen_data.keys.first
53
+ args = gen_data[handle]
54
+ end
55
+
56
+ raise ArgumentError, "an attribute name must be specified" unless handle = handle.to_sym
57
+
58
+ unless args.is_a?(Hash)
59
+ unless block
60
+ retval = args
61
+ block = lambda { retval } # lambda { args } results in returning the empty hash that args gets changed to
62
+ end
63
+ args = {} # args is assumed to be a hash for the rest of the method
64
+ end
65
+
66
+ if args[:start]
67
+ block ||= lambda { |prev| prev.succ }
68
+ end
69
+
70
+ if args[:method]
71
+ h = { :method => args[:method].to_sym }
72
+ h[:start] = args[:start] if args[:start]
73
+ record_generator_for(handle, h)
74
+ elsif args[:class]
75
+ raise ArgumentError, "generator class [#{args[:class].name}] does not have a :next method" unless args[:class].respond_to?(:next)
76
+ record_generator_for(handle, :class => args[:class])
77
+ elsif block
78
+ 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
79
+ h = { :block => block }
80
+ h[:start] = args[:start] if args[:start]
81
+ record_generator_for(handle, h)
82
+ else
83
+ raise ArgumentError, "a block, :class generator, :method generator, or value must be specified to generator_for"
84
+ end
85
+ end
86
+
87
+ def generates_subclass(subclass_name)
88
+ @concrete_subclass_name = subclass_name.to_s
89
+ end
90
+
91
+ def gather_exemplars
92
+ return if exemplars_generated
93
+ if superclass.respond_to?(:gather_exemplars)
94
+ superclass.gather_exemplars
95
+ self.generators = (superclass.generators || {}).dup
96
+ end
97
+
98
+ [*exemplar_path].each do |raw_path|
99
+ path = File.join(raw_path, "#{underscore(name)}_exemplar.rb")
100
+ load(path) if File.exists?(path)
101
+ end
102
+ self.exemplars_generated = true
103
+ end
104
+
105
+ def presence_validated_attributes
106
+ @presence_validated_attributes ||= {}
107
+ attrs = @presence_validated_attributes
108
+ if superclass.respond_to?(:presence_validated_attributes)
109
+ attrs = superclass.presence_validated_attributes.merge(attrs)
110
+ end
111
+ attrs
112
+ end
113
+
114
+ protected
115
+
116
+ # we define an underscore helper ourselves since the ActiveSupport isn't available if we're not using Rails
117
+ def underscore(string)
118
+ string.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
119
+ end
120
+
121
+ def record_generator_for(handle, generator)
122
+ self.generators ||= {}
123
+ raise ArgumentError, "a generator for attribute [:#{handle}] has already been specified" if (generators[handle] || {})[:source] == self
124
+ generators[handle] = { :generator => generator, :source => self }
125
+ end
126
+
127
+ private
128
+
129
+ def generate_values(args)
130
+ (generators || {}).each_pair do |handle, gen_data|
131
+ next if args.include?(handle) or args.include?(handle.to_s)
132
+ generator = gen_data[:generator]
133
+ if generator[:block]
134
+ process_generated_value(args, handle, generator, generator[:block])
135
+ elsif generator[:method]
136
+ method = method(generator[:method])
137
+ if method.arity == 1
138
+ process_generated_value(args, handle, generator, method)
139
+ else
140
+ args[handle] = method.call
141
+ end
142
+ elsif generator[:class]
143
+ args[handle] = generator[:class].next
144
+ end
145
+ end
146
+
147
+ generate_missing(args)
148
+ end
149
+
150
+ def process_generated_value(args, handle, generator, block)
151
+ if generator[:start]
152
+ value = generator[:start]
153
+ generator.delete(:start)
154
+ else
155
+ value = block.call(generator[:prev])
156
+ end
157
+ generator[:prev] = args[handle] = value
158
+ end
159
+
160
+ def generate_missing(args)
161
+ if presence_validated_attributes and !presence_validated_attributes.empty?
162
+ req = {}
163
+ (presence_validated_attributes.keys - args.keys).each {|a| req[a.to_s] = true } # find attributes required by validates_presence_of not already set
164
+
165
+ belongs_to_associations = reflect_on_all_associations(:belongs_to).to_a
166
+ missing = belongs_to_associations.select { |a| req[a.name.to_s] or req[a.primary_key_name.to_s] }
167
+ if create_scope = scope(:create)
168
+ missing.reject! { |a| create_scope.include?(a.primary_key_name) }
169
+ end
170
+ missing.reject! { |a| [a.name, a.primary_key_name].any? { |n| args.stringify_keys.include?(n.to_s) } }
171
+ missing.each {|a| args[a.name] = a.class_name.constantize.generate }
172
+ end
173
+ end
174
+ end
175
+
176
+ module RailsClassMethods
177
+ def exemplar_path
178
+ dir = File.directory?(File.join(RAILS_ROOT, 'spec')) ? 'spec' : 'test'
179
+ File.join(RAILS_ROOT, dir, 'exemplars')
180
+ end
181
+
182
+ def validates_presence_of_with_object_daddy(*attr_names)
183
+ @presence_validated_attributes ||= {}
184
+ new_attr = attr_names.dup
185
+ new_attr.pop if new_attr.last.is_a?(Hash)
186
+ new_attr.each {|a| @presence_validated_attributes[a] = true }
187
+ validates_presence_of_without_object_daddy(*attr_names)
188
+ end
189
+
190
+ # :call-seq:
191
+ # generate()
192
+ # generate() do |obj| ... end
193
+ # generate(args)
194
+ # generate(args) do |obj| ... end
195
+ #
196
+ # Creates and tries to save an instance of this class, using any known
197
+ # generators. The generated instance is yielded to a block if provided.
198
+ #
199
+ # This will not raise errors on a failed save. Use generate! if you
200
+ # want errors raised.
201
+ def generate(args = {})
202
+ spawn(args) do |instance|
203
+ instance.save
204
+ yield instance if block_given?
205
+ end
206
+ end
207
+
208
+ # :call-seq:
209
+ # generate()
210
+ # generate() do |obj| ... end
211
+ # generate(args)
212
+ # generate(args) do |obj| ... end
213
+ #
214
+ # Creates and tries to save! an instance of this class, using any known
215
+ # generators. The generated instance is yielded to a block if provided.
216
+ #
217
+ # This will raise errors on a failed save. Use generate if you do not want
218
+ # errors raised.
219
+ def generate!(args = {})
220
+ spawn(args) do |instance|
221
+ instance.save!
222
+ yield instance if block_given?
223
+ end
224
+ end
225
+ end
226
+ end
227
+
228
+ unless ActiveRecord::Base.respond_to? :inherited_with_object_daddy
229
+ class ActiveRecord::Base
230
+ def self.inherited_with_object_daddy(subclass)
231
+ self.inherited_without_object_daddy(subclass)
232
+ subclass.send(:include, ObjectDaddy) unless subclass < ObjectDaddy
233
+ end
234
+
235
+ class << self
236
+ alias_method_chain :inherited, :object_daddy
237
+ end
238
+ end
239
+ end