-
Notifications
You must be signed in to change notification settings - Fork 16
/
README
307 lines (231 loc) · 10.5 KB
/
README
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
= Object Daddy
<b>Version 0.2.9 (November 13, 2008)</b>
Author:: Rick Bradley (mailto:blogicx@rickbradley.com), Yossef Mendelssohn (ymendel@pobox.com)
Copyright:: Copyright (c) 2007, Flawed Logic, OG Consulting, Rick Bradley, Yossef Mendelssohn
License:: MIT License. See MIT-LICENSE file for more details.
Object Daddy is a library (as well as a Ruby on Rails plugin) designed to
assist in automating testing of large collections of objects, especially webs
of ActiveRecord models. It is a descendent of the "Object Mother" pattern for
creating objects for testing, and is related to the concept of an "object
exemplar" or "stereotype".
WARNING: This code is very much at an "alpha" development stage. Usage, APIs,
etc., are all subject to change.
See http://b.logi.cx/2007/11/26/object-daddy for inspiration, historical drama, and too much reading.
== Installation
1. Unpack the object_daddy directory into vendor/rails/ in your rails project.
2. Run the object_daddy/install.rb Ruby script.
A. Or, as an alternative and assuming your script/plugin has git support,
script/plugin install git://github.com/flogic/object_daddy.git
== Testing
Install the rspec gem and cd into the object_daddy directory. Type 'spec
spec/' and you should see all specs run successfully. If you have autotest
from the ZenTest gem installed you can run autotest in that directory.
== Using Object Daddy
Object Daddy adds a .generate method to every ActiveRecord model which can be
called to generate a valid instance object of that model class, for use in
testing:
it "should have a comment for every forum the user posts to" do
@user = User.generate
@post = Post.generate
@post.comments << Comment.generate
@user.should have(1).comments
end
This allows us to generate custom model objects without relying on fixtures,
and without knowing, in our various widespread tests and specs, the details of
creating a User, Post, Comment, etc. Not having to know this information means
the information isn't coded into dozens (or hundreds) of tests, and won't need
to be changed when the User (Post, Comment, ...) model is refactored later.
Object Daddy will identify associated classes that need to be instantiated to
make the main model valid. E.g., given the following models:
class User < ActiveRecord::Base
belongs_to :login
validates_presence_of :login
end
class Login < ActiveRecord::Base
has_one :user
end
a call to User.generate will also make a call to Login.generate so that
User#login is present, and therefore valid.
If all models were able to be created in a valid form by the default Model.new
call with no knowledge of the model itself, there'd be no need for Object
Daddy. So, when we deal with models which have validity requirements,
requiring fields which have format constraints, we need a means of expressing
how to create those models -- how to satisfy those validity constraints.
Object Daddy provides a "generator_for" method which allows the developer to
specify, for a specific model attribute, how to make a valid value. Note that
"validates_uniqueness_of" can require that, even if we make 100,000 instances
of a model that unique attributes cannot have the same values.
Object Daddy's generator_for method can take three main forms corresponding to
the means of finding a value for the associated attribute: a block, a method
call, or using a generator class.
class User < ActiveRecord::Base
validates_presence_of :email
validates_uniqueness_of :email
validates_format_of :email,
:with => /^[-a-z_+0-9.]+@(?:[-a-z_+0-9.]\.)+[a-z]+$/i
validates_presence_of :username
validates_format_of :username, :with => /^[a-z0-9_]{4,12}$/i
generator_for :email, :start => 'test@domain.com' do |prev|
user, domain = prev.split('@')
user.succ + '@' + domain
end
generator_for :username, :method => :next_user
generator_for :ssn, :class => SSNGenerator
def self.next_user
@last_username ||= 'testuser'
@last_username.succ
end
end
class SSNGenerator
def self.next
@last ||= '000-00-0000'
@last = ("%09d" % (@last.gsub('-', '').to_i + 1)).sub(/^(\d{3})(\d{2})(\d{4})$/, '\1-\2-\3')
end
end
Note that the block method of invocation (as used with :email above) takes an
optional :start argument, to specify the value of that attribute on the first
run. The block will be called thereafter with the previous value of the
attribute and will generate the next attribute value to be used.
A simple default block is provided for any generator with a :start value.
class User < ActiveRecord::Base
generator_for :name, :start => 'Joe' do |prev|
prev.succ
end
generator_for :name, :start => 'Joe' # equivalent to the above
end
The :method form takes a symbol naming a class method in the model class to be
called to generate a new value for the attribute in question. If the method
takes a single argument, it will act much like the block method of invocation,
being called with the previous value and generating the next.
The :class form calls the .next class method on the named class to generate a
new value for the attribute in question.
The argument (previous value) to the block invocation form can be omitted if
it's going to be ignored, and simple invocation forms are provided for literal
values.
class User < ActiveRecord::Base
generator_for(:start_time) { Time.now }
generator_for :name, 'Joe'
generator_for :age => 25
end
The developer would then simply call 'User.generate' when testing.
If some attribute values are known (or are being controlled during testing)
then these can simply be passed in to .generate:
@bad_login = Login.generate(:expiry => 1.week.ago)
@expired_user = User.generate(:login => @bad_login)
A .generate! method is also provided. The generate/generate! pair of methods
can be thought of as analogs to create/create!, one merely providing an instance
that may or may not be valid and the other raising an exception if any
problem comes up.
Finally, a .spawn method is provided that only gives a new, unsaved object. Note
that this is the only method of the three that is available if you happen to be
using Object Daddy outside of Rails.
== Exemplars
In the examples given above we are using 'generator_for' in the bodies of the
models themselves. Given that Object Daddy is primarily geared towards
annotating models with information useful for testing, we anticipate that
generator_for should not normally be included inline in models. Rather, we
will provide a place where model classes can be re-opened and generator_for
calls (and support methods) can be written without polluting the model files
with Object Daddy information.
Object Daddy, when installed as a Rails plugin, will create
RAILS_ROOT/spec/exemplars/ as a place to hold "exemplar" files for Rails model
classes. (We are seeking perhaps some better terminology)
An exemplar for the User model would then be found in
RAILS_ROOT/spec/exemplars/user_exemplar.rb (when you are using a testing tool
which works from RAILS_ROOT/test, Object Daddy will create
RAILS_ROOT/test/exemplars and look for your exemplars in that directory
instead). Exemplar files are completely optional, and no model need have
exemplar files. The .generate method will still exist and be callable, and
generator_for can be declared in the model files themselves. If an exemplar
file is available when .generate is called on a model, the exemplar file will
be loaded and used. An example user_exemplar.rb appears below:
require 'ssn_generator'
class User < ActiveRecord::Base
generator_for :email, :start => 'test@domain.com' do |prev|
user, domain = prev.split('@')
user.succ + '@' + domain
end
generator_for :username, :method => :next_user
generator_for :ssn, :class => SSNGenerator
def self.next_user
@last_username ||= 'testuser'
@last_username.succ
end
end
== Blocks
The spawn, generate and generate! methods can all accept a block, to which
they'll yield the generated object. This provides a nice scoping mechanism in
your code examples. Consider:
describe "admin user" do
it "should be authorized to create company profiles"
admin_user = User.generate!
admin_user.activate!
admin_user.add_role("admin")
admin_user.should be_authorized(:create, Company)
end
end
This could be refactored to:
describe "admin user" do
it "should be authorized to create company profiles" do
admin_user = User.generate! do |user|
user.activate!
user.add_role("admin")
end
admin_user.should be_authorized(:create, Company)
end
end
Or:
describe "admin user" do
it "should be authorized to create company profiles"
User.generate! do |user|
user.activate!
user.add_role("admin")
end.should be_authorized(:create, Company)
end
end
Or even:
describe "admin user" do
def admin_user
@admin_user ||= User.generate! do |user|
user.activate!
user.add_role("admin")
end
end
it "should be authorized to create company profiles"
admin_user.should be_authorized(:create, Company)
end
end
This last refactoring allows you to reuse the admin_user method across
multiple code examples, balancing DRY with local data.
== Object Daddy and Fixtures
While Object Daddy is meant to obviate the hellish devilspawn that are test
fixtures, Object Daddy should work alongside fixtures just fine. To each his
own, I suppose.
== Known Issues
The simple invocation forms for generator_for when using literal values do not
work if the literal value is a Hash. Don't do that.
class User < ActiveRecord::Base
generator_for :thing_hash, { 'some key' => 'some value' }
generator_for :other_hash => { 'other key' => 'other value' }
end
I'm not sure why this would even ever come up, but seriously, don't.
Required belongs_to associations are automatically generated when generating an instance,
but only if necessary.
class Category < ActiveRecord::Base
has_many :items
end
class Item < ActiveRecord::Base
belongs_to :category
validates_presence_of :category
end
Item.generate will generate a new category, but some_category.items.generate will not.
Unless, of course, you are foolish enough to define a generator in the exemplar.
class Item
generator_for(:category) { Category.generate }
end
Once again, don't do that.
=== Rails 'surprises'
Due to the way Rails handles associations, cascading generations (as a result of
required associations) are always generated-and-saved, even if the original generation
call was a mere spawn ('new'). This may come as a surprise, but it would probably be more
of a surprise if User.spawn.save and User.generate weren't comparable.