dupe 0.3.7 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/README.rdoc +390 -147
  2. data/lib/dupe/active_resource_extensions.rb +25 -0
  3. data/lib/dupe/attribute_template.rb +71 -0
  4. data/lib/dupe/cucumber_hooks.rb +15 -7
  5. data/lib/dupe/custom_mocks.rb +102 -0
  6. data/lib/dupe/database.rb +51 -0
  7. data/lib/dupe/dupe.rb +359 -372
  8. data/lib/dupe/log.rb +38 -0
  9. data/lib/dupe/mock.rb +50 -0
  10. data/lib/dupe/model.rb +55 -0
  11. data/lib/dupe/network.rb +38 -0
  12. data/lib/dupe/record.rb +35 -56
  13. data/lib/dupe/rest_validation.rb +16 -0
  14. data/lib/dupe/schema.rb +36 -0
  15. data/lib/dupe/sequence.rb +11 -10
  16. data/lib/dupe/singular_plural_detection.rb +9 -0
  17. data/lib/dupe/string.rb +6 -8
  18. data/lib/dupe/symbol.rb +3 -0
  19. data/lib/dupe.rb +13 -12
  20. data/rails_generators/dupe/templates/custom_mocks.rb +4 -34
  21. data/rails_generators/dupe/templates/dupe_setup.rb +3 -23
  22. data/spec/lib_specs/active_resource_extensions_spec.rb +29 -0
  23. data/spec/lib_specs/attribute_template_spec.rb +173 -0
  24. data/spec/lib_specs/database_spec.rb +133 -0
  25. data/spec/lib_specs/dupe_spec.rb +307 -0
  26. data/spec/lib_specs/log_spec.rb +78 -0
  27. data/spec/lib_specs/logged_request_spec.rb +22 -0
  28. data/spec/lib_specs/mock_definitions_spec.rb +32 -0
  29. data/spec/lib_specs/mock_spec.rb +67 -0
  30. data/spec/lib_specs/model_spec.rb +90 -0
  31. data/spec/lib_specs/network_spec.rb +77 -0
  32. data/spec/lib_specs/record_spec.rb +70 -0
  33. data/spec/lib_specs/rest_validation_spec.rb +17 -0
  34. data/spec/lib_specs/schema_spec.rb +90 -0
  35. data/spec/lib_specs/sequence_spec.rb +26 -0
  36. data/spec/lib_specs/string_spec.rb +31 -0
  37. data/spec/lib_specs/symbol_spec.rb +17 -0
  38. data/spec/spec_helper.rb +2 -5
  39. metadata +29 -7
  40. data/lib/dupe/active_resource.rb +0 -135
  41. data/lib/dupe/attribute.rb +0 -17
  42. data/lib/dupe/configuration.rb +0 -20
  43. data/lib/dupe/mock_service_response.rb +0 -55
  44. data/spec/lib_specs/dupe_record_spec.rb +0 -57
data/README.rdoc CHANGED
@@ -1,30 +1,25 @@
1
1
  = Dupe
2
2
 
3
3
  There are lots of great tools out there to ease the burden of prototyping ActiveRecord objects while cuking your application (e.g., thoughtbot's {"Factory Girl"}[http://www.thoughtbot.com/projects/factory_girl]).
4
+
4
5
  But what about prototyping ActiveResource records? That's where Dupe steps in.
5
6
 
6
7
 
7
8
  == Motivation
8
9
 
9
- Dupe is ideally suited for cuking the client side of a service-oriented (ActiveResource) application where the backend service does not yet exist.
10
+ Dupe is ideally suited for cuking the client side of a service-oriented (ActiveResource) application.
10
11
 
11
- Why would anyone want to do that, you might ask? Dupe arose in the midst of a site migration project. Essentially, the site being replaced
12
- had a complicated, denormalized, and chaotic backend datastore that made ordinary data migration a complex nightmare. So the developers involved
13
- decided to rebuild the frontend of the site first, and connect it to the existing backend via services.
14
12
 
15
- Using Dupe, they were able to cuke the new frontend, mocking
16
- the backend services they needed, and then the developers for the old site were able to use the mocked XML output of Dupe to determine exactly what
17
- services they needed to expose on the backend (along with the XML format of those services). Once those services existed, the new frontend was launched,
18
- and the developers then created a new backend, migrated data from the old backend (over the services), and pointed the new frontend to the new backend services.
19
- The resulting site had a nicely decoupled, service-oriented architecture.
13
+ == Installation
20
14
 
15
+ If you want to install this for use in something other than a rails project, simply:
21
16
 
22
- == Installation
17
+ # gem install dupe
23
18
 
24
- Add this to your cucumber.rb environment (config/environments/cucumber.rb)
19
+ If you're going to use this in a rails project, add this to your cucumber.rb environment (config/environments/cucumber.rb)
20
+
21
+ config.gem 'dupe', :lib => 'dupe', :version => '>=0.4.0'
25
22
 
26
- config.gem 'dupe', :lib => 'dupe', :version => '>=0.3.5' :source => 'http://gemcutter.org'
27
-
28
23
  Then run this rake task to install the gem:
29
24
 
30
25
  # rake gems:install RAILS_ENV=cucumber
@@ -32,157 +27,405 @@ Then run this rake task to install the gem:
32
27
  Lastly, from your rails project root, run:
33
28
 
34
29
  # script/generate dupe
30
+
31
+
32
+ = Features
33
+
34
+ ==Creating resources
35
+
36
+ Dupe allows you to quickly create resources, even if you have yet to define them. For example:
37
+
38
+ irb# require 'dupe'
39
+ ==> true
40
+
41
+ irb# b = Dupe.create :book, :title => '2001'
42
+ ==> <#Duped::Book title="2001" id=1>
43
+
44
+ irb# a = Dupe.create :author, :name => 'Arthur C. Clarke'
45
+ ==> <#Duped::Author name="Arthur C. Clarke" id=1>
46
+
47
+ irb# b.author
48
+ ==> nil
49
+
50
+ irb# b.author = a
51
+ ==> <#Duped::Author name="Arthur C. Clarke" id=1>
52
+
53
+ irb# b
54
+ ==> <#Duped::Book author=<#Duped::Author name="Arthur C. Clarke" id=1> title="2001" id=1>
55
+
56
+
57
+ Dupe also provides a way for us to quickly to generate a large number of resources. For example, suppose we have a cucumber scenario that tests paginating through lists of books. To easily create 50 unique books, we could use the Dupe.stub method:
58
+
59
+ irb# Dupe.stub 50, :books, :like => {:title => proc {|n| "book ##{n} title"}}
60
+ ==> [<#Duped::Book title="book #1 title" id=1>, <#Duped::Book title="book #2 title" id=2>, ...]
61
+
62
+ Notice that each book has a unique title, achieved by passing the "proc {|n| "book ##{n} title"}" as the value for the title.
63
+
64
+
65
+ ==Finding Resources
35
66
 
67
+ Dupe also has a built-in querying system for finding resources you create. In your tests / cucumber step definitions, you'll most likely be using this approach for finding resources. If you're wondering how your app (i.e., ActiveResource) can find resources you create, skip down to the section on ActiveResource.
36
68
 
37
- == Example
69
+ irb# a = Dupe.create :author, :name => 'Monkey'
70
+ ==> <#Duped::Author name="Monkey" id=1>
38
71
 
39
- Let's suppose your cuking a book search application for a library that consumes a RESTFUL book datastore service via ActiveResource.
40
- You might start by writing the following feature in <em>RAILS_ROOT/features/library/find_book.feature</em>:
72
+ irb# b = Dupe.create :book, :title => 'Bananas', :author => a
73
+ ==> <#Duped::Book author=<#Duped::Author name="Monkey" id=1> title="Bananas" id=1>
41
74
 
42
- Feature: find a book
43
- As a reader
44
- I want to search for books
45
- so that I can check them out and read them.
75
+ irb# Dupe.find(:author) {|a| a.name == 'Monkey'}
76
+ ==> <#Duped::Author name="Monkey" id=1>
77
+
78
+ irb# Dupe.find(:book) {|b| b.author.name == 'Monkey'}
79
+ ==> <#Duped::Book author=<#Duped::Author name="Monkey" id=1> title="Bananas" id=1>
80
+
81
+ irb# Dupe.find(:author) {|a| a.id == 1}
82
+ ==> <#Duped::Author name="Monkey" id=1>
83
+
84
+ irb# Dupe.find(:author) {|a| a.id == 2}
85
+ ==> nil
86
+
87
+ In all cases, notice that we provided the singular form of a model name to Dupe.find. This ensures that we either get back either a single resource (if the query was successful), or _nil_.
88
+
89
+ If we'd like to find several resources, we can use the plural form of the model name. For example:
90
+
91
+ irb# a = Dupe.create :author, :name => 'Monkey', :published => true
92
+ ==> <#Duped::Author published=true name="Monkey" id=1>
93
+
94
+ irb# b = Dupe.create :book, :title => 'Bananas', :author => a
95
+ ==> <#Duped::Book author=<#Duped::Author published=true name="Monkey" id=1> title="Bananas" id=1>
96
+
97
+ irb# Dupe.create :author, :name => 'Tiger', :published => false
98
+ ==> <#Duped::Author published=false name="Tiger" id=2>
99
+
100
+ irb# Dupe.find(:authors)
101
+ ==> [<#Duped::Author published=true name="Monkey" id=1>, <#Duped::Author published=false name="Tiger" id=2>]
102
+
103
+ irb# Dupe.find(:authors) {|a| a.published == true}
104
+ ==> [<#Duped::Author published=true name="Monkey" id=1>]
105
+
106
+ irb# Dupe.find(:books)
107
+ ==> [<#Duped::Book author=<#Duped::Author published=true name="Monkey" id=1> title="Bananas" id=1>]
46
108
 
47
- Scenario: search by author
48
- Given an author "Arthur C. Clarke"
49
- And a book "2001: A Space Odyssey" by "Arthur C. Clarke"
50
- When I search for "Arthur C. Clarke"
51
- I should see "2001: A Space Odyssey"
109
+ irb# Dupe.find(:books) {|b| b.author.published == false}
110
+ ==> []
111
+
112
+ Notice that by using the plural form of the model name, we ensure that we receive back an array - even in the case that the query did not find any results (it simply returns an empty array).
52
113
 
53
- To get this to pass, you might first create ActiveResource[http://api.rubyonrails.org/classes/ActiveResource/Base.html] models for Books and Authors that connect to the Library service:
54
114
 
55
- class Book < ActiveResource::Base
56
- self.site = 'http://bookservice.domain'
57
- end
115
+ ==Finding or Creating Resources
58
116
 
59
- class Author < ActiveResource::Base
60
- self.site = 'http://bookservice.domain'
61
- end
117
+ You might have seen this one coming:
62
118
 
63
- Then you might create the following resource definition via Dupe.define (put it in a file with a .rb extension and place it in RAILS_ROOT/features/support/):
119
+ irb# Dupe.find :genre
120
+ Dupe::Database::TableDoesNotExistError: The table ':genre' does not exist.
121
+ from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/database.rb:30:in `select'
122
+ from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/dupe.rb:295:in `find'
123
+ from (irb):40
124
+
125
+ irb# Dupe.find_or_create :genre
126
+ ==> <#Duped::Genre id=1>
127
+
128
+ irb# Dupe.find_or_create :genre
129
+ ==> <#Duped::Genre id=1>
130
+
131
+ You can also pass conditions to find_or_create as a hash:
132
+
133
+ irb# Dupe.find_or_create :genre, :name => 'Science Fiction', :label => 'sci-fi'
134
+ ==> <#Duped::Genre label="sci-fi" name="Science Fiction" id=2>
135
+
136
+ irb# Dupe.find_or_create :genre, :name => 'Science Fiction', :label => 'sci-fi'
137
+ ==> <#Duped::Genre label="sci-fi" name="Science Fiction" id=2>
138
+
139
+ == Defining a resource
140
+
141
+ Though often we may get away with creating resources willy-nilly, it's sometimes quite handy to define a resource, giving it default attributes and callbacks.
142
+
143
+ === Attributes with default values
144
+
145
+ Suppose we're creating a 'book' resource. Perhaps our app assumes every book has a title, so let's define a book resource
146
+ that specifies just that:
147
+
148
+ irb# Dupe.define :book do |attrs|
149
+ --# attrs.title 'Untitled'
150
+ --# attrs.author
151
+ --# end
152
+ ==> #<Dupe::Model:0x17b2694 ...>
64
153
 
65
- Dupe.define :book do |book|
66
- book.author do |author_name|
67
- Dupe.find(:author) {|a| a.name == author_name}
68
- end
69
- end
154
+ Basically, this reads like "A book resource has a title attribute with a default value of 'Untitled'. It also has an author attribute." Thus, if we create a book and we don't specify a "title" attribute, it should create a "title" for us, as well as provide a nil "author" attribute.
70
155
 
71
- and the following cucumber step definitions (utilizing Dupe.create):
156
+ irb# b = Dupe.create :book
157
+ ==> <#Duped::Book author=nil title="Untitled" id=1>
158
+
159
+
160
+ If we provide our own title, it should allow us to override the default value:
161
+
162
+ irb# b = Dupe.create :book, :title => 'Monkeys!'
163
+ ==> <#Duped::Book author=nil title="Monkeys!" id=2>
164
+
165
+ === Attributes with procs as default values
166
+
167
+ Sometimes it might be convenient to procedurally define the default value for an attribute:
168
+
169
+ irb# Dupe.define :book do |attrs|
170
+ --# attrs.title 'Untitled'
171
+ --# attrs.author
172
+ --# attrs.isbn do
173
+ --# rand(1000000)
174
+ --# end
175
+ --# end
176
+
177
+ Now, every time we create a book, it will get assigned a random ISBN number:
178
+
179
+ irb# b = Dupe.create :book
180
+ ==> <#Duped::Book author=nil title="Untitled" id=1 isbn=895825>
181
+
182
+ irb# b = Dupe.create :book
183
+ ==> <#Duped::Book author=nil title="Untitled" id=2 isbn=606472>
184
+
185
+ Another common use of this feature is for associations. Lets suppose we'd like to make sure that a book always has a genre, but a genre should be it's own resource. We can accomplish that by taking advantage of Dupe's "find_or_create" method:
186
+
187
+ irb# Dupe.define :book do |attrs|
188
+ --# attrs.title 'Untitled'
189
+ --# attrs.author
190
+ --# attrs.isbn do
191
+ --# rand(1000000)
192
+ --# end
193
+ --# attrs.genre do
194
+ --# Dupe.find_or_create :genre
195
+ --# end
196
+ --# end
197
+
198
+ Now when we create books, Dupe will associate them with an existing genre (the first one it finds), or if none yet exist, it will create one.
199
+
200
+ First, let's confirm that no genres currently exist:
201
+
202
+ irb# Dupe.find :genre
203
+ Dupe::Database::TableDoesNotExistError: The table ':genre' does not exist.
204
+ from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/database.rb:30:in `select'
205
+ from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/dupe.rb:295:in `find'
206
+ from (irb):135
207
+
208
+ Next, let's create a book:
209
+
210
+ irb# b = Dupe.create :book
211
+ ==> <#Duped::Book genre=<#Duped::Genre id=1> author=nil title="Untitled" id=1 isbn=62572>
212
+
213
+ Notice that it create a genre. If we tried to do another Dupe.find for the genre:
214
+
215
+ irb# Dupe.find :genre
216
+ ==> <#Duped::Genre id=1>
217
+
218
+ Now, if create another book, it will associate with the genre that was just created:
219
+
220
+ irb# b = Dupe.create :book
221
+ ==> <#Duped::Book genre=<#Duped::Genre id=1> author=nil title="Untitled" id=2 isbn=729317>
222
+
223
+
224
+
225
+ === Attributes with transformers
226
+
227
+ Occasionally, you may find it useful to have attribute values transformed upon creation.
228
+
229
+ For example, suppose we want to create books with publish dates. In our cucumber scenario's, we may prefer to simply specify a date like '2009-12-29', and have that automatically transformed into an ruby Date object.
230
+
231
+ irb# Dupe.define :book do |attrs|
232
+ --# attrs.title 'Untitled'
233
+ --# attrs.author
234
+ --# attrs.isbn do
235
+ --# rand(1000000)
236
+ --# end
237
+ --# attrs.publish_date do |publish_date|
238
+ --# Date.parse(publish_date)
239
+ --# end
240
+ --# end
241
+
242
+ Now, let's create a book:
243
+
244
+ irb# b = Dupe.create :book, :publish_date => '2009-12-29'
245
+ ==> <#Duped::Book author=nil title="Untitled" publish_date=Tue, 29 Dec 2009 id=1 isbn=826291>
246
+
247
+ irb# b.publish_date
248
+ ==> Tue, 29 Dec 2009
249
+
250
+ irb# b.publish_date.class
251
+ ==> Date
252
+
253
+ === Callbacks
254
+
255
+ Suppose we'd like to make sure that our books get a unique label. We can accomplish that with an after_create callback:
256
+
257
+ irb# Dupe.define :book do |attrs|
258
+ --# attrs.title 'Untitled'
259
+ --# attrs.author
260
+ --# attrs.isbn do
261
+ --# rand(1000000)
262
+ --# end
263
+ --# attrs.publish_date do |publish_date|
264
+ --# Date.parse(publish_date)
265
+ --# end
266
+ --# attrs.after_create do |book|
267
+ --# book.label = book.title.downcase.gsub(/\ +/, '-') + "--#{book.id}"
268
+ --# end
269
+ --# end
270
+
271
+ irb# b = Dupe.create :book, :title => 'Rooby on Rails'
272
+ ==> <#Duped::Book author=nil label="rooby-on-rails--1" title="Rooby on Rails" publish_date=nil id=1 isbn=842518>
273
+
274
+
275
+ = ActiveResource
276
+
277
+ So how does Dupe actually help us to spec/test ActiveResource-based applications? It uses a simple, yet sophisticated "intercept-mocking" technique, whereby failed network requests sent by ActiveResource fallback to the "Duped" network. Consider the following:
278
+
279
+ irb# Dupe.create :book, :title => 'Monkeys!'
280
+ ==> <#Duped::Book title="Monkeys!" id=1>
281
+
282
+ irb# class Book < ActiveResource::Base; self.site = ''; end
283
+ ==> ""
284
+
285
+ irb# Book.find(1)
286
+ ==> #<Book:0x1868a20 @attributes={"title"=>"Monkeys!", "id"=>1}, prefix_options{}
287
+
288
+ Voila! When the _Book_ class was unable to find the book with id 1, it asked Dupe if it knew about any book resources with id 1. Check out the Dupe network log for a clue as to what happened behind the scenes:
289
+
290
+ irb# puts Dupe.network.log.pretty_print
291
+
292
+ Logged Requests:
293
+ Request: GET /books/1.xml
294
+ Response:
295
+ <?xml version="1.0" encoding="UTF-8"?>
296
+ <book>
297
+ <title>Monkeys!</title>
298
+ <id type="integer">1</id>
299
+ </book>
300
+
301
+ Similarly:
302
+
303
+ irb# Book.find(:all)
304
+ ==> [#<Book:0x185608c @attributes={"title"=>"Monkeys!", "id"=>1}, prefix_options{}]
305
+
306
+ irb# puts Dupe.network.log.pretty_print
307
+
308
+ Logged Requests:
309
+ Request: GET /books.xml
310
+ Response:
311
+ <?xml version="1.0" encoding="UTF-8"?>
312
+ <books type="array">
313
+ <book>
314
+ <title>Monkeys!</title>
315
+ <id type="integer">1</id>
316
+ </book>
317
+ </books>
318
+
319
+
320
+ ==Intercept Mocking
321
+
322
+ Dupe knew how to handle simple find by id and find :all lookups from ActiveResource. But what about other requests we might potentially make?
323
+
324
+ irb# Dupe.create :author, :name => 'Monkey', :published => true
325
+ ==> <#Duped::Author name="Monkey" published=true id=1>
326
+
327
+ irb# Dupe.create :author, :name => 'Tiger', :published => false
328
+ ==> <#Duped::Author name="Tiger" published=false id=2>
329
+
330
+ irb# class Author < ActiveResource::Base; self.site = ''; end
331
+ ==> ""
332
+
333
+ irb# Author.find :all, :from => :published
334
+ Dupe::Network::RequestNotFoundError: No mocked service response found for '/authors/published.xml'
335
+ from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/network.rb:32:in `match'
336
+ from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/network.rb:17:in `request'
337
+ from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/active_resource_extensions.rb:15:in `get'
338
+ from /Library/Ruby/Gems/1.8/gems/activeresource-2.3.5/lib/active_resource/custom_methods.rb:57:in `get'
339
+ from /Library/Ruby/Gems/1.8/gems/activeresource-2.3.5/lib/active_resource/base.rb:632:in `find_every'
340
+ from /Library/Ruby/Gems/1.8/gems/activeresource-2.3.5/lib/active_resource/base.rb:582:in `find'
341
+ from (irb):12
342
+
343
+ Obviously, Dupe had no way of anticipating this possibility. However, you can create your own custom intercept mock for this:
344
+
345
+ irb# Get %r{/authors/published.xml} do
346
+ --# Dupe.find(:authors) {|a| a.published == true}
347
+ --# end
348
+ ==> #<Dupe::Network::Mock:0x1833e88 @url_pattern=/\/authors\/published.xml/, @verb=:get, @response=#<Proc:0x01833f14@(irb):13>
349
+
350
+ irb# Author.find :all, :from => :published
351
+ ==> [#<Author:0x1821d3c @attributes={"name"=>"Monkey", "published"=>true, "id"=>1}, prefix_options{}]
352
+
353
+ irb# puts Dupe.network.log.pretty_print
354
+
355
+ Logged Requests:
356
+ Request: GET /authors/published.xml
357
+ Response:
358
+ <?xml version="1.0" encoding="UTF-8"?>
359
+ <authors type="array">
360
+ <author>
361
+ <name>Monkey</name>
362
+ <published type="boolean">true</published>
363
+ <id type="integer">1</id>
364
+ </author>
365
+ </authors>
366
+
367
+
368
+ The "Get" method requires a url pattern and a block. In most cases, your block will return a Dupe.find result. Internally, Dupe will transform that into XML. However, if your "Get" block returns a string, Dupe will use that as the response body and not attempt to do any transformations on it.
369
+
370
+ Suppose instead the service expected us to pass published as a query string parameter:
371
+
372
+ irb# Author.find :all, :params => {:published => true}
373
+ Dupe::Network::RequestNotFoundError: No mocked service response found for '/authors.xml?published=true'
374
+ from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/network.rb:32:in `match'
375
+ from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/network.rb:17:in `request'
376
+ from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/active_resource_extensions.rb:15:in `get'
377
+ from /Library/Ruby/Gems/1.8/gems/activeresource-2.3.5/lib/active_resource/base.rb:639:in `find_every'
378
+ from /Library/Ruby/Gems/1.8/gems/activeresource-2.3.5/lib/active_resource/base.rb:582:in `find'
379
+ from (irb):18
380
+
381
+ We can mock this with the following:
382
+
383
+ irb# Get %r{/authors\.xml\?published=(true|false)$} do |published|
384
+ --# if published == 'true'
385
+ --# Dupe.find(:authors) {|a| a.published == true}
386
+ --# else
387
+ --# Dupe.find(:authors) {|a| a.published == false}
388
+ --# end
389
+ --# end
390
+
391
+ irb# Author.find :all, :params => {:published => true}
392
+ ==> [#<Author:0x17db094 @attributes={"name"=>"Monkey", "published"=>true, "id"=>1}, prefix_options{}]
393
+
394
+ irb# Author.find :all, :params => {:published => false}
395
+ ==> [#<Author:0x17c68c4 @attributes={"name"=>"Tiger", "published"=>false, "id"=>2}, prefix_options{}]
396
+
397
+ irb# puts Dupe.network.log.pretty_print
72
398
 
73
- Given /^an author "([^\"]*)"$/ do |author|
74
- Dupe.create :author, :name => author
75
- end
76
-
77
- Given /^a book "([^\"]*)" by "([^\"]*)"$/ do |book, author|
78
- Dupe.create :book, :title => book, :author => author
79
- end
80
-
81
- Dupe.create will in turn mock two service responses for each resource. For example,
82
- for the Book resource, it will mock:
83
-
84
- # Book.find(:all) --> GET /books.xml
85
- <?xml version="1.0" encoding="UTF-8"?>
86
- <books type="array">
87
- <book>
88
- <id type="integer">1</id>
89
- <title>2001: A Space Odyssey</title>
90
- <author>
91
- <id type="integer">1</id>
92
- <name>Arthur C. Clarke</name>
93
- </author>
94
- </book>
95
- </books>
96
-
97
- # Book.find(1) --> GET /books/1.xml
98
- <?xml version="1.0" encoding="UTF-8"?>
99
- <book>
100
- <id type="integer">1</id>
101
- <title>2001: A Space Odyssey</title>
102
- <author>
103
- <id type="integer">1</id>
104
- <name>Arthur C. Clarke</name>
105
- </author>
106
- </book>
107
-
108
- From here, you could start scaffolding your controllers, with the assumption that Dupe will mock the responses to Book.find(<id or :all>) and Author.find(<id or :all>).
109
-
110
- However, what happens when one of your controllers uses an ActiveResource object in such a way that it sends off a request that Dupe has not yet mocked? For example, suppose
111
- you had the following in one of your controllers:
112
-
113
- Book.find :all, :params => {:author_id => 1}
114
-
115
- This would send off a request like "/books.xml?author_id=1". If this happened during the context of a cucumber scenario, then Dupe would throw an exception with a message like:
116
-
117
- There is no custom service mapping for "/books.xml?author_id=1".
118
- Now go to features/support/custom_mocks.rb and add it. (StandardError)
119
-
120
- When you ran "script/generate dupe" during dupe installation, it created a file features/support/custom_mocks.rb that should look something like this:
121
-
122
- module CustomMocks
123
- # Maps a service request url to a Dupe find. By default, Dupe will only
124
- # mock simple requests like SomeResource.find(some_id) or SomeResource.find(:all)
125
- #
126
- # For example, suppose you have a Book < ActiveResource::Base class, and
127
- # somewhere your code does:
128
- #
129
- # Book.find :all, :params => {:limit => 10, :offset => 20}
130
- #
131
- # That in turn will send off a request to a url like:
132
- #
133
- # /books.xml?limit=10&offset=20
134
- #
135
- # In this file, you could add a "when" statement like:
136
- #
137
- # when %r{/books.xml\?limit=(\d+)&offset=(\d+)$}
138
- # start = $2.to_i
139
- # finish = start + $1.to_i - 1
140
- # Dupe.find(:books)[start..finish]
141
- def custom_service(url)
142
- case url
143
-
144
- # remove this and replace it with a real custom mock
145
- when %r{/bogus_url}
146
- ''
147
-
148
- else
149
- raise StandardError.new(
150
- "There is no custom service mapping for \"#{url}\"." +
151
- "Now go to features/support/custom_mocks.rb and add it."
152
- )
153
- end
154
- end
155
- end
156
-
157
-
158
- Now remove the "when %r{/bogus_url}" statement and add a custom mock for your service:
159
-
160
- def custom_service(url)
161
- case url
162
-
163
- when %r{books.xml\?author_id=(\d+)$}
164
- Dupe.find(:books) {|b| b.author.id == $1}
165
-
166
- else
167
- raise StandardError.new(
168
- "There is no custom service mapping for \"#{url}\"." +
169
- "Now go to features/support/custom_mocks.rb and add it."
170
- )
171
-
172
- end
173
- end
174
-
175
- Dupe will take the result of the Dupe.find(:books)... code (a Hash), and convert it to XML.
176
- You could have alternatively given a string of XML (or a string of anything), and Dupe would have taken it as-is.
399
+ Logged Requests:
400
+ Request: GET /authors.xml?published=true
401
+ Response:
402
+ <?xml version="1.0" encoding="UTF-8"?>
403
+ <authors type="array">
404
+ <author>
405
+ <name>Monkey</name>
406
+ <published type="boolean">true</published>
407
+ <id type="integer">1</id>
408
+ </author>
409
+ </authors>
410
+
411
+ Request: GET /authors.xml?published=false
412
+ Response:
413
+ <?xml version="1.0" encoding="UTF-8"?>
414
+ <authors type="array">
415
+ <author>
416
+ <name>Tiger</name>
417
+ <published type="boolean">false</published>
418
+ <id type="integer">2</id>
419
+ </author>
420
+ </authors>
177
421
 
178
422
 
179
423
  == More
180
424
 
181
- Dupe supports attribute defaults, attribute transformations, stubbing, resource associations, custom resource mocks, and more.
182
- Want to learn more? Consult the API documentation at http://moonmaster9000.github.com/dupe/api/
425
+ Consult the API documentation at http://moonmaster9000.github.com/dupe/api/
183
426
 
184
427
 
185
428
  == TODO List
186
429
 
187
- * Thus far, Dupe has only been used for consuming a service via GET requests. There are no facilities yet for mocking responses to PUT, POST, and DELETE requests.
188
- * We need a rake task that will run your scenarios and create service documentation based on the dupe log output (i.e., example requests and example responses) that the programmers implementing the service can use as a reference.
430
+ * We need "Put", "Post", and "Delete", and "Head" intercept mocking methods. Currently we only have "Get".
431
+ * We need a rake task that will run your cucumber scenarios and create service documentation based on the dupe log output (i.e., example requests and example responses) that the programmers implementing the service can use as a reference.
@@ -0,0 +1,25 @@
1
+ ActiveResource::HttpMock.instance_eval do #:nodoc:
2
+ def delete_mock(http_method, path) #:nodoc:
3
+ responses.reject! {|r| r[0].path == path && r[0].method == http_method}
4
+ end
5
+ end
6
+
7
+ module ActiveResource #:nodoc:
8
+ class Connection #:nodoc:
9
+ def get(path, headers = {}) #:nodoc:
10
+ begin
11
+ response = request(:get, path, build_request_headers(headers, :get))
12
+
13
+ # if the request threw an exception
14
+ rescue
15
+ mocked_response = Dupe.network.request(:get, path)
16
+ ActiveResource::HttpMock.respond_to do |mock|
17
+ mock.get path, {}, mocked_response
18
+ end
19
+ response = request(:get, path, build_request_headers(headers, :get))
20
+ ActiveResource::HttpMock.delete_mock(:get, path)
21
+ end
22
+ format.decode(response.body)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,71 @@
1
+ class Dupe
2
+ class Model #:nodoc:
3
+ class Schema #:nodoc:
4
+ # This class represents an attribute template.
5
+ # An attribute template consists of an attribute name (a symbol),
6
+ # a potential default value (nil if not specified),
7
+ # and a potential transformer proc.
8
+ class AttributeTemplate #:nodoc:
9
+
10
+ class NilValue; end
11
+
12
+ attr_reader :name
13
+ attr_reader :transformer
14
+ attr_reader :default
15
+
16
+ def initialize(name, options={})
17
+ default = options[:default]
18
+ transformer = options[:transformer]
19
+
20
+ if transformer
21
+ raise ArgumentError, "Your transformer must be a kind of proc." if !transformer.kind_of?(Proc)
22
+ raise ArgumentError, "Your transformer must accept a parameter." if transformer.arity != 1
23
+ end
24
+
25
+ @name = name
26
+ @default = default
27
+ @transformer = transformer
28
+ end
29
+
30
+ # Suppose we have the following attribute template:
31
+ #
32
+ # console> a = Dupe::Model::Schema::AttributeTemplate.new(:title)
33
+ #
34
+ # If we call generate with no parameter, we'll get back the following:
35
+ #
36
+ # console> a.generate
37
+ # ===> :title, nil
38
+ #
39
+ # If we call generate with a parameter, we'll get back the following:
40
+ #
41
+ # console> a.generate 'my value'
42
+ # ===> :title, 'my value'
43
+ #
44
+ # If we setup an attribute template with a transformer:
45
+ #
46
+ # console> a =
47
+ # Dupe::Model::Schema::AttributeTemplate.new(
48
+ # :title,
49
+ # :default => 'default value',
50
+ # :transformer => proc {|dont_care| 'test'}
51
+ # )
52
+ # Then we'll get back the following when we call generate:
53
+ #
54
+ # console> a.generate
55
+ # ===> :title, 'default value'
56
+ #
57
+ # console> a.generate 'my value'
58
+ # ===> :title, 'test'
59
+ def generate(value=NilValue)
60
+ if value == NilValue
61
+ v = @default.respond_to?(:call) ? @default.call : @default
62
+ else
63
+ v = (@transformer ? @transformer.call(value) : value)
64
+ end
65
+
66
+ return @name, v
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end