dupe 0.3.7 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +390 -147
- data/lib/dupe/active_resource_extensions.rb +25 -0
- data/lib/dupe/attribute_template.rb +71 -0
- data/lib/dupe/cucumber_hooks.rb +15 -7
- data/lib/dupe/custom_mocks.rb +102 -0
- data/lib/dupe/database.rb +51 -0
- data/lib/dupe/dupe.rb +359 -372
- data/lib/dupe/log.rb +38 -0
- data/lib/dupe/mock.rb +50 -0
- data/lib/dupe/model.rb +55 -0
- data/lib/dupe/network.rb +38 -0
- data/lib/dupe/record.rb +35 -56
- data/lib/dupe/rest_validation.rb +16 -0
- data/lib/dupe/schema.rb +36 -0
- data/lib/dupe/sequence.rb +11 -10
- data/lib/dupe/singular_plural_detection.rb +9 -0
- data/lib/dupe/string.rb +6 -8
- data/lib/dupe/symbol.rb +3 -0
- data/lib/dupe.rb +13 -12
- data/rails_generators/dupe/templates/custom_mocks.rb +4 -34
- data/rails_generators/dupe/templates/dupe_setup.rb +3 -23
- data/spec/lib_specs/active_resource_extensions_spec.rb +29 -0
- data/spec/lib_specs/attribute_template_spec.rb +173 -0
- data/spec/lib_specs/database_spec.rb +133 -0
- data/spec/lib_specs/dupe_spec.rb +307 -0
- data/spec/lib_specs/log_spec.rb +78 -0
- data/spec/lib_specs/logged_request_spec.rb +22 -0
- data/spec/lib_specs/mock_definitions_spec.rb +32 -0
- data/spec/lib_specs/mock_spec.rb +67 -0
- data/spec/lib_specs/model_spec.rb +90 -0
- data/spec/lib_specs/network_spec.rb +77 -0
- data/spec/lib_specs/record_spec.rb +70 -0
- data/spec/lib_specs/rest_validation_spec.rb +17 -0
- data/spec/lib_specs/schema_spec.rb +90 -0
- data/spec/lib_specs/sequence_spec.rb +26 -0
- data/spec/lib_specs/string_spec.rb +31 -0
- data/spec/lib_specs/symbol_spec.rb +17 -0
- data/spec/spec_helper.rb +2 -5
- metadata +29 -7
- data/lib/dupe/active_resource.rb +0 -135
- data/lib/dupe/attribute.rb +0 -17
- data/lib/dupe/configuration.rb +0 -20
- data/lib/dupe/mock_service_response.rb +0 -55
- 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
|
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
|
-
|
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
|
-
|
17
|
+
# gem install dupe
|
23
18
|
|
24
|
-
|
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
|
-
|
69
|
+
irb# a = Dupe.create :author, :name => 'Monkey'
|
70
|
+
==> <#Duped::Author name="Monkey" id=1>
|
38
71
|
|
39
|
-
|
40
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
56
|
-
self.site = 'http://bookservice.domain'
|
57
|
-
end
|
115
|
+
==Finding or Creating Resources
|
58
116
|
|
59
|
-
|
60
|
-
self.site = 'http://bookservice.domain'
|
61
|
-
end
|
117
|
+
You might have seen this one coming:
|
62
118
|
|
63
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
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
|
-
*
|
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
|