moonmaster9000-dupe 0.1.2
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.rdoc +84 -0
- data/lib/dupe/active_resource.rb +27 -0
- data/lib/dupe/attribute.rb +17 -0
- data/lib/dupe/configuration.rb +22 -0
- data/lib/dupe/cucumber_hooks.rb +3 -0
- data/lib/dupe/dupe.rb +524 -0
- data/lib/dupe/mock_service_response.rb +52 -0
- data/lib/dupe/record.rb +11 -0
- data/lib/dupe/sequence.rb +11 -0
- data/lib/dupe.rb +17 -0
- metadata +81 -0
data/README.rdoc
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
= Dupe
|
|
2
|
+
|
|
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
|
+
But what about prototyping ActiveResource records? That's where Dupe steps in.
|
|
5
|
+
|
|
6
|
+
== Installation
|
|
7
|
+
|
|
8
|
+
# gem install moonmaster9000-dupe
|
|
9
|
+
|
|
10
|
+
== Example
|
|
11
|
+
Let's suppose your cuking a book search application for a library that consumes a RESTFUL book datastore service via ActiveResource.
|
|
12
|
+
You might start by writing the following feature in <em>RAILS_ROOT/features/library/find_book.feature</em>:
|
|
13
|
+
|
|
14
|
+
Feature: find a book
|
|
15
|
+
As a reader
|
|
16
|
+
I want to search for books
|
|
17
|
+
so that I can check them out and read them.
|
|
18
|
+
|
|
19
|
+
Scenario: search by author
|
|
20
|
+
Given an author "Arthur C. Clarke"
|
|
21
|
+
And a book "2001: A Space Odyssey" by "Arthur C. Clarke"
|
|
22
|
+
When I search for "Arthur C. Clarke"
|
|
23
|
+
I should see "2001: A Space Odyssey"
|
|
24
|
+
|
|
25
|
+
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:
|
|
26
|
+
|
|
27
|
+
class Book < ActiveResource::Base
|
|
28
|
+
self.site = 'http://bookservice.domain'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class Author < ActiveResource::Base
|
|
32
|
+
self.site = 'http://bookservice.domain'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
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/):
|
|
36
|
+
|
|
37
|
+
Dupe.define :book do |book|
|
|
38
|
+
book.author do |author_name|
|
|
39
|
+
Dupe.find(:author) {|a| a.name == author_name}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
and the following cucumber step definitions (utilizing Dupe.create):
|
|
44
|
+
|
|
45
|
+
Given /^an author "([^\"]*)"$/ do |author|
|
|
46
|
+
Dupe.create :author, :name => author
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Given /^a book "([^\"]*)" by "([^\"]*)"$/ do |book, author|
|
|
50
|
+
Dupe.create :book, :title => book, :author => author
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
Dupe.create will in turn mock two service responses for each resource. For example,
|
|
54
|
+
for the Book resource, it will mock:
|
|
55
|
+
|
|
56
|
+
# Book.find(:all) --> GET /books.xml
|
|
57
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
58
|
+
<books type="array">
|
|
59
|
+
<book>
|
|
60
|
+
<id type="integer">1</id>
|
|
61
|
+
<title>2001: A Space Odyssey</title>
|
|
62
|
+
<author>
|
|
63
|
+
<id type="integer">1</id>
|
|
64
|
+
<name>Arthur C. Clarke</name>
|
|
65
|
+
</author>
|
|
66
|
+
</book>
|
|
67
|
+
</books>
|
|
68
|
+
|
|
69
|
+
# Book.find(1) --> GET /books/1.xml
|
|
70
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
71
|
+
<book>
|
|
72
|
+
<id type="integer">1</id>
|
|
73
|
+
<title>2001: A Space Odyssey</title>
|
|
74
|
+
<author>
|
|
75
|
+
<id type="integer">1</id>
|
|
76
|
+
<name>Arthur C. Clarke</name>
|
|
77
|
+
</author>
|
|
78
|
+
</book>
|
|
79
|
+
|
|
80
|
+
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>).
|
|
81
|
+
|
|
82
|
+
== More
|
|
83
|
+
|
|
84
|
+
Dupe supports attribute defaults, attribute transoformations, stubbing, resource associations, custom resource mocks, and more. Want to learn more? Consult the API documentation at http://moonmaster9000.github.com/dupe/api/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# this allows us to control when we flush out the HttpMock requests / responses
|
|
2
|
+
ActiveResource::HttpMock.instance_eval do #:nodoc:
|
|
3
|
+
def reset! #:nodoc:
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def reset_from_dupe! #:nodoc:
|
|
7
|
+
requests.clear
|
|
8
|
+
responses.clear
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# makes it possible to override existing request/response definitions
|
|
13
|
+
module ActiveResource #:nodoc:
|
|
14
|
+
class HttpMock #:nodoc:
|
|
15
|
+
class Responder #:nodoc:
|
|
16
|
+
for method in [ :post, :put, :get, :delete, :head ]
|
|
17
|
+
module_eval <<-EOE, __FILE__, __LINE__
|
|
18
|
+
def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
|
|
19
|
+
@responses.reject! {|r| r[0].path == path && r[0].method == :#{method}}
|
|
20
|
+
@responses << [Request.new(:#{method}, path, nil, request_headers), Response.new(body || "", status, response_headers)]
|
|
21
|
+
end
|
|
22
|
+
EOE
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class Dupe
|
|
2
|
+
class Attribute #:nodoc:
|
|
3
|
+
def initialize(name, value=nil, prock=nil)
|
|
4
|
+
@name, @value, @prock = name.to_sym, value, prock
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def value(v=nil)
|
|
8
|
+
v = @value.dup if @value and !v
|
|
9
|
+
@prock && v ? @prock.call(v) : v
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_hash
|
|
13
|
+
{@name => value}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class Dupe
|
|
2
|
+
class Configuration #:nodoc:
|
|
3
|
+
attr_reader :config
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@config ||= {}
|
|
7
|
+
@config[:record_identifiers] = [:id]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def method_missing(method_name, *args, &block)
|
|
11
|
+
set_config_option(method_name.to_sym, args)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
def set_config_option(key, value)
|
|
17
|
+
@config[key] = value
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
data/lib/dupe/dupe.rb
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
# Dupe allows you to define resources, create a pool of resources,
|
|
2
|
+
# extend those resources with your own custom response mocks, and even override the default
|
|
3
|
+
# mocks Dupe provides (<em>find(:all)</em> and <em>find(id)</em>).
|
|
4
|
+
#
|
|
5
|
+
# Dupe is ideally suited for working with Cucumber[http://cukes.info]. It also relies on ActiveResource::HttpMock[http://api.rubyonrails.org/classes/ActiveResource/HttpMock.html] for mocking
|
|
6
|
+
# resource responses.
|
|
7
|
+
#
|
|
8
|
+
# Let's suppose your cuking a book search application for a library that consumes a RESTFUL book datastore service via ActiveResource.
|
|
9
|
+
# You might start by writing the following feature in <em>RAILS_ROOT/features/library/find_book.feature</em>:
|
|
10
|
+
#
|
|
11
|
+
# Feature: find a book
|
|
12
|
+
# As a reader
|
|
13
|
+
# I want to search for books
|
|
14
|
+
# so that I can check them out and read them.
|
|
15
|
+
#
|
|
16
|
+
# Scenario: search by author
|
|
17
|
+
# Given an author "Arthur C. Clarke"
|
|
18
|
+
# And a book "2001: A Space Odyssey" by "Arthur C. Clarke"
|
|
19
|
+
# When I search for "Arthur C. Clarke"
|
|
20
|
+
# I should see "2001: A Space Odyssey"
|
|
21
|
+
#
|
|
22
|
+
# To get this to pass, you might first create an ActiveResource[http://api.rubyonrails.org/classes/ActiveResource/Base.html] model for a Book and an Author that will connect
|
|
23
|
+
# to the RESTful book service:
|
|
24
|
+
#
|
|
25
|
+
# class Book < ActiveResource::Base
|
|
26
|
+
# self.site = 'http://bookservice.domain'
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# class Author < ActiveResource::Base
|
|
30
|
+
# self.site = 'http://bookservice.domain'
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# 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/):
|
|
34
|
+
#
|
|
35
|
+
# Dupe.define :book do |define|
|
|
36
|
+
# define.author do |author_name|
|
|
37
|
+
# Dupe.find(:author) {|a| a.name == author_name}
|
|
38
|
+
# end
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# and the following cucumber step definitions (utilizing Dupe.create):
|
|
42
|
+
#
|
|
43
|
+
# Given /^an author "([^\"]*)"$/ do |author|
|
|
44
|
+
# Dupe.create :author, :name => author
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# Given /^a book "([^\"]*)" by "([^\"]*)"$/ do |book, author|
|
|
48
|
+
# Dupe.create :book, :title => book, :author => author
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# Dupe.create will in turn mock two service responses for each resource. For example,
|
|
52
|
+
# for the Book resource, it will mock:
|
|
53
|
+
#
|
|
54
|
+
# # Book.find(:all) --> GET /books.xml
|
|
55
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
|
56
|
+
# <books type="array">
|
|
57
|
+
# <book>
|
|
58
|
+
# <id type="integer">1</id>
|
|
59
|
+
# <title>2001: A Space Odyssey</title>
|
|
60
|
+
# <author>
|
|
61
|
+
# <id type="integer">1</id>
|
|
62
|
+
# <name>Arthur C. Clarke</name>
|
|
63
|
+
# </author>
|
|
64
|
+
# </book>
|
|
65
|
+
# </books>
|
|
66
|
+
#
|
|
67
|
+
# # Book.find(1) --> GET /books/1.xml
|
|
68
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
|
69
|
+
# <book>
|
|
70
|
+
# <id type="integer">1</id>
|
|
71
|
+
# <title>2001: A Space Odyssey</title>
|
|
72
|
+
# <author>
|
|
73
|
+
# <id type="integer">1</id>
|
|
74
|
+
# <name>Arthur C. Clarke</name>
|
|
75
|
+
# </author>
|
|
76
|
+
# </book>
|
|
77
|
+
|
|
78
|
+
# Author:: Matt Parker (mailto:moonmaster9000@gmail.com)
|
|
79
|
+
# License:: Distributes under the same terms as Ruby
|
|
80
|
+
|
|
81
|
+
class Dupe
|
|
82
|
+
attr_reader :factory_name #:nodoc:
|
|
83
|
+
attr_reader :configuration #:nodoc:
|
|
84
|
+
attr_reader :attributes #:nodoc:
|
|
85
|
+
attr_reader :config #:nodoc:
|
|
86
|
+
attr_reader :mocker #:nodoc:
|
|
87
|
+
attr_reader :records #:nodoc:
|
|
88
|
+
|
|
89
|
+
class << self
|
|
90
|
+
attr_accessor :factories #:nodoc:
|
|
91
|
+
|
|
92
|
+
# Create data definitions for your resources. This allows you to setup default values for columns
|
|
93
|
+
# and even provide data transformations.
|
|
94
|
+
#
|
|
95
|
+
# For example, suppose you had the following cucumber scenario:
|
|
96
|
+
#
|
|
97
|
+
# # RAILS_ROOT/features/library/find_book.feature
|
|
98
|
+
# Feature: Find a book
|
|
99
|
+
# As a reader
|
|
100
|
+
# I want to find books in my library
|
|
101
|
+
# So that I can read them
|
|
102
|
+
#
|
|
103
|
+
# Scenario: Browsing books
|
|
104
|
+
# Given the following author:
|
|
105
|
+
# | name | date_of_birth |
|
|
106
|
+
# | Arthur C. Clarke | 1917-12-16 |
|
|
107
|
+
#
|
|
108
|
+
# And the following book:
|
|
109
|
+
# | name | author |
|
|
110
|
+
# | 2001: A Space Odyssey | Arthur C. Clarke |
|
|
111
|
+
#
|
|
112
|
+
# When....
|
|
113
|
+
#
|
|
114
|
+
#
|
|
115
|
+
# We can use Dupe.define to
|
|
116
|
+
# * Transform data (e.g., turn the string '1917-12-16' into a Date object)
|
|
117
|
+
# * Provide default values for attributes (e.g., give all author's a default biography)
|
|
118
|
+
# * Associate records (e.g., given an author name, return the author record associated with that name)
|
|
119
|
+
#
|
|
120
|
+
# To accomplish the afore mentioned definitions:
|
|
121
|
+
#
|
|
122
|
+
# # RAILS_ROOT/features/dupe_definitions/book.rb
|
|
123
|
+
#
|
|
124
|
+
# Dupe.define :author do |define|
|
|
125
|
+
# define.bio 'Lorem ipsum delor.'
|
|
126
|
+
# define.date_of_birth do |d|
|
|
127
|
+
# Date.parse(t)
|
|
128
|
+
# end
|
|
129
|
+
# end
|
|
130
|
+
#
|
|
131
|
+
# Dupe.define :book do |define|
|
|
132
|
+
# define.author do |author_name|
|
|
133
|
+
# Dupe.find(:author) {|a| a.name == author_name}
|
|
134
|
+
# end
|
|
135
|
+
# end
|
|
136
|
+
#
|
|
137
|
+
# -----------------------------------------------------------------------------------------------------------------
|
|
138
|
+
#
|
|
139
|
+
# # RAILS_ROOT/features/step_definitions/library/find_book_steps.rb
|
|
140
|
+
#
|
|
141
|
+
# Given /^the following author:$/ do |author_table|
|
|
142
|
+
# Dupe.create(:author, author_table.hashes)
|
|
143
|
+
# end
|
|
144
|
+
#
|
|
145
|
+
# Given /^the following book:$/ do |book_table|
|
|
146
|
+
# Dupe.create(:book, book_table.hashes)
|
|
147
|
+
# end
|
|
148
|
+
#
|
|
149
|
+
# When cucumber encounters the "Given the following author:" line, the corresponding step definition
|
|
150
|
+
# will ask Dupe to mock ActiveResource responses to find(:all) and find(:id) with the data
|
|
151
|
+
# specified in the cucumber hash table immediately following the "Given the following author:" line.
|
|
152
|
+
# Since we didn't specify a 'bio' value in our cucumber hash table, Dupe will give it the
|
|
153
|
+
# default value 'Bio stub.'. Also, it will transform the 'date_of_birth' value we provided in the hash
|
|
154
|
+
# table into a time object.
|
|
155
|
+
#
|
|
156
|
+
# Similarly, for the :book cucumber hash table, Dupe will transform the author name we provided
|
|
157
|
+
# into the author object we had already specified in the :author table.
|
|
158
|
+
#
|
|
159
|
+
# In terms of mocked responses, we could expect something like:
|
|
160
|
+
#
|
|
161
|
+
# # Author.find(1) --> GET /authors/1.xml
|
|
162
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
|
163
|
+
# <author>
|
|
164
|
+
# <id type="integer">1</id>
|
|
165
|
+
# <name>Arthur C. Clarke</name>
|
|
166
|
+
# <bio>Bio stub.</bio>
|
|
167
|
+
# <date_of_birth>1917-12-16T00:00:00Z</date_of_birth>
|
|
168
|
+
# </author>
|
|
169
|
+
#
|
|
170
|
+
# # Book.find(1) --> GET /books/1.xml
|
|
171
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
|
172
|
+
# <book>
|
|
173
|
+
# <id type="integer">1</id>
|
|
174
|
+
# <name>2001: A Space Odyssey</name>
|
|
175
|
+
# <author>
|
|
176
|
+
# <id type="integer">1</id>
|
|
177
|
+
# <name>Arthur C. Clarke</name>
|
|
178
|
+
# <bio>Bio stub.</bio>
|
|
179
|
+
# <date_of_birth>1917-12-16T00:00:00Z</date_of_birth>
|
|
180
|
+
# </author>
|
|
181
|
+
# </book>
|
|
182
|
+
def define(factory) # yield: define
|
|
183
|
+
setup_factory(factory)
|
|
184
|
+
yield @factories[factory]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# This method will cause Dupe to mock resources for the record(s) provided.
|
|
188
|
+
# The "records" value may be either a hash or an array of hashes.
|
|
189
|
+
# For example, suppose you'd like to mock a single author ActiveResource object:
|
|
190
|
+
#
|
|
191
|
+
# Dupe.create :author, :name => 'Arthur C. Clarke'
|
|
192
|
+
#
|
|
193
|
+
# This will translate into the following two mocked resource calls:
|
|
194
|
+
#
|
|
195
|
+
# # Author.find(:all) --> GET /authors.xml
|
|
196
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
|
197
|
+
# <authors>
|
|
198
|
+
# <author>
|
|
199
|
+
# <id type="integer">1</id>
|
|
200
|
+
# <name>Arthur C. Clarke</name>
|
|
201
|
+
# </author>
|
|
202
|
+
# </authors>
|
|
203
|
+
#
|
|
204
|
+
# # Author.find(1) --> GET /authors/1.xml
|
|
205
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
|
206
|
+
# <author>
|
|
207
|
+
# <id type="integer">1</id>
|
|
208
|
+
# <name>Arthur C. Clarke</name>
|
|
209
|
+
# </author>
|
|
210
|
+
#
|
|
211
|
+
# However, suppose you wanted to mock two or more authors.
|
|
212
|
+
#
|
|
213
|
+
# Dupe.create :author, [{:name => 'Arthur C. Clarke'}, {:name => 'Robert Heinlein'}]
|
|
214
|
+
#
|
|
215
|
+
# This will translate into the following three mocked resource calls:
|
|
216
|
+
#
|
|
217
|
+
# # Author.find(:all) --> GET /authors.xml
|
|
218
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
|
219
|
+
# <authors>
|
|
220
|
+
# <author>
|
|
221
|
+
# <id type="integer">1</id>
|
|
222
|
+
# <name>Arthur C. Clarke</name>
|
|
223
|
+
# </author>
|
|
224
|
+
# <author>
|
|
225
|
+
# <id type="integer">2</id>
|
|
226
|
+
# <name>Robert Heinlein</name>
|
|
227
|
+
# </author>
|
|
228
|
+
# </authors>
|
|
229
|
+
#
|
|
230
|
+
# # Author.find(1) --> GET /authors/1.xml
|
|
231
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
|
232
|
+
# <author>
|
|
233
|
+
# <id type="integer">1</id>
|
|
234
|
+
# <name>Arthur C. Clarke</name>
|
|
235
|
+
# </author>
|
|
236
|
+
#
|
|
237
|
+
# # Author.find(2) --> GET /authors/2.xml
|
|
238
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
|
239
|
+
# <author>
|
|
240
|
+
# <id type="integer">2</id>
|
|
241
|
+
# <name>Robert Heinlein</name>
|
|
242
|
+
# </author>
|
|
243
|
+
def create(factory, records={})
|
|
244
|
+
setup_factory(factory)
|
|
245
|
+
raise Exception, "unknown records type" if !records.nil? and !records.is_a?(Array) and !records.is_a?(Hash)
|
|
246
|
+
records = [records] if records.is_a?(Hash)
|
|
247
|
+
@factories[factory].generate_services_for(records)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# You can use this method to quickly stub out a large number of resources. For example:
|
|
251
|
+
#
|
|
252
|
+
# Dupe.stub(
|
|
253
|
+
# :author,
|
|
254
|
+
# :template => {:name => 'author'},
|
|
255
|
+
# :count => 20
|
|
256
|
+
# )
|
|
257
|
+
#
|
|
258
|
+
# would generate 20 author records like:
|
|
259
|
+
#
|
|
260
|
+
# {:name => 'author 1', :id => 1}
|
|
261
|
+
# ....
|
|
262
|
+
# {:name => 'author 20', :id => 20}
|
|
263
|
+
#
|
|
264
|
+
# and it would also mock find(id) and find(:all) responses for these records (along with any other custom mocks you've
|
|
265
|
+
# setup via Dupe.define_mocks).
|
|
266
|
+
#
|
|
267
|
+
# You may override both the sequence starting value and the attribute to sequence:
|
|
268
|
+
#
|
|
269
|
+
# Dupe.stub(
|
|
270
|
+
# :book,
|
|
271
|
+
# :template => {:author => 'Arthur C. Clarke', :title => 'moonmaster'},
|
|
272
|
+
# :count => 20,
|
|
273
|
+
# :sequence_start_value => 9000,
|
|
274
|
+
# :sequence => :title
|
|
275
|
+
# )
|
|
276
|
+
#
|
|
277
|
+
# This would generate 20 book records like:
|
|
278
|
+
#
|
|
279
|
+
# {:id => 1, :author => 'Arthur C. Clarke', :title => 'moonmaster 9000'}
|
|
280
|
+
# ....
|
|
281
|
+
# {:id => 20, :author => 'Arthur C. Clarke', :title => 'moonmaster 9019'}
|
|
282
|
+
#
|
|
283
|
+
# Naturally, stub will consult the Dupe.define definitions for anything it's attempting to stub
|
|
284
|
+
# and will honor those definitions (default values, transformations) as you would expect.
|
|
285
|
+
def stub(factory, options)
|
|
286
|
+
setup_factory(factory)
|
|
287
|
+
@factories[factory].stub_services_with(options[:template], options[:count].to_i, (options[:sequence_start_value] || 1), options[:sequence])
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# This allows you to override the array record identifiers for your resources ([:id], by default)
|
|
291
|
+
#
|
|
292
|
+
# For example, suppose the RESTful application your trying to consume supports lookups by both a textual 'label'
|
|
293
|
+
# and a numeric 'id', and that it contains an author service where the author with id '1' has the label 'arthur-c-clarke'.
|
|
294
|
+
# Your application should expect the same response whether or not you call <tt>Author.find(1)</tt> or <tt>Author.find('arthur-c-clarke')</tt>.
|
|
295
|
+
#
|
|
296
|
+
# Thus, to ensure that Dupe mocks both, do the following:
|
|
297
|
+
# Dupe.configure :author do |configure|
|
|
298
|
+
# configure.record_identifiers :id, :label
|
|
299
|
+
# end
|
|
300
|
+
#
|
|
301
|
+
# With this configuration, a <tt>Dupe.create :author, :name => 'Arthur C. Clarke', :label => 'arthur-c-clarke'</tt>
|
|
302
|
+
# will result in the following mocked service calls:
|
|
303
|
+
#
|
|
304
|
+
# <tt>Author.find(1) --> (GET /authors/1.xml)</tt>
|
|
305
|
+
#
|
|
306
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
|
307
|
+
# <author>
|
|
308
|
+
# <id type="integer">1</id>
|
|
309
|
+
# <name>Arthur C. Clarke</name>
|
|
310
|
+
# <label>arthur-c-clarke</label>
|
|
311
|
+
# </author>
|
|
312
|
+
#
|
|
313
|
+
#
|
|
314
|
+
# <tt>Author.find('arthur-c-clarke') --> (GET /authors/arthur-c-clarke.xml)</tt>
|
|
315
|
+
#
|
|
316
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
|
317
|
+
# <author>
|
|
318
|
+
# <id type="integer">1</id>
|
|
319
|
+
# <name>Arthur C. Clarke</name>
|
|
320
|
+
# <label>arthur-c-clarke</label>
|
|
321
|
+
# </author>
|
|
322
|
+
def configure(factory) # yield: configure
|
|
323
|
+
setup_factory(factory)
|
|
324
|
+
yield @factories[factory].config
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# By default, Dupe will mock responses to ActiveResource <tt>find(:all)</tt> and <tt>find(id)</tt>.
|
|
328
|
+
# However, it's likely that your cucumber scenarios will eventually fire off an ActiveResource request that's
|
|
329
|
+
# something other than these basic lookups.
|
|
330
|
+
#
|
|
331
|
+
# Dupe.define_mocks allows you to add new resource mocks and override the built-in resource mocks.
|
|
332
|
+
#
|
|
333
|
+
# For example, suppose you had a Book ActiveResource model, and you want to use it to get the :count of all
|
|
334
|
+
# Books in the back end system your consuming. <tt>Book.get(:count)</tt> would fire off an HTTP request to the
|
|
335
|
+
# backend service like <tt>"GET /books/count.xml"</tt>, and assuming the service is set up to respond to that
|
|
336
|
+
# request, you might expect to get something back like:
|
|
337
|
+
#
|
|
338
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
|
339
|
+
# <hash>
|
|
340
|
+
# <count type="integer">3</count>
|
|
341
|
+
# </hash>
|
|
342
|
+
#
|
|
343
|
+
# To mock this for the purposes of cuking, you could do the following:
|
|
344
|
+
#
|
|
345
|
+
# Dupe.define_mocks :book do |define|
|
|
346
|
+
# define.count do |mock, records|
|
|
347
|
+
# mock.get "/books/count.xml", {}, {:count => records.size}.to_xml
|
|
348
|
+
# end
|
|
349
|
+
# end
|
|
350
|
+
#
|
|
351
|
+
# The <tt>mock</tt> object is the ActiveResource::HttpMock object. Please see the documentation for that
|
|
352
|
+
# if you would like to know more about what's possible with it.
|
|
353
|
+
def define_mocks(factory) # yield: define
|
|
354
|
+
setup_factory(factory)
|
|
355
|
+
yield @factories[factory].mocker
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# Search for a resource. This works a bit differently from both ActiveRecord's find and ActiveResource's find.
|
|
360
|
+
# This is most often used for defining associations between objects (Dupe.define).
|
|
361
|
+
# It will return a hash representation of the resource (or an array of hashes if we asked for multiple records).
|
|
362
|
+
#
|
|
363
|
+
# For example, suppose we have an author resource, and a book resource with a nested author attribute (in ActiveRecord
|
|
364
|
+
# parlance, Book belongs_to Author, Author has_many Book).
|
|
365
|
+
#
|
|
366
|
+
# Now suppose we've created the following cucumber scenario:
|
|
367
|
+
#
|
|
368
|
+
# Scenario: Browsing books
|
|
369
|
+
# Given the following author:
|
|
370
|
+
# | name | date_of_birth |
|
|
371
|
+
# | Arthur C. Clarke | 1917-12-16 |
|
|
372
|
+
#
|
|
373
|
+
# And the following books:
|
|
374
|
+
# | name | author | published | genre |
|
|
375
|
+
# | 2001: A Space Odyssey | Arthur C. Clarke | 1968 | sci-fi |
|
|
376
|
+
# | A Fall of Moondust | Arthur C. Clarke | 1961 | fantasy |
|
|
377
|
+
# | Rendezvous with Rama | Arthur C. Clarke | 1972 | sci-fi |
|
|
378
|
+
#
|
|
379
|
+
# When....
|
|
380
|
+
#
|
|
381
|
+
# To link up the book and author, we could create the following book definition
|
|
382
|
+
#
|
|
383
|
+
# Dupe.define :book do |book|
|
|
384
|
+
# book.author {|name| Dupe.find(:author) {|a| a.name == name}}
|
|
385
|
+
# end
|
|
386
|
+
#
|
|
387
|
+
# The line <tt>Dupe.find(:author) {|a| a.name == name}</tt> could be translated as
|
|
388
|
+
# "find the first author record where the author's name equals `name`".
|
|
389
|
+
#
|
|
390
|
+
# Dupe decided to return only a single record because we specified <tt>find(:author)</tt>.
|
|
391
|
+
# Had we instead specified <tt>find(:authors)</tt>, resource factory would have instead returned an array of results.
|
|
392
|
+
#
|
|
393
|
+
# More examples:
|
|
394
|
+
#
|
|
395
|
+
# # find all books written in the 1960's
|
|
396
|
+
# Dupe.find(:books) {|b| b.published >= 1960 and b.published <= 1969}
|
|
397
|
+
#
|
|
398
|
+
# # return the first book found that was written by Arthur C. Clarke (nested resources example)
|
|
399
|
+
# Dupe.find(:book) {|b| b.author.name == 'Arthur C. Clarke'}
|
|
400
|
+
#
|
|
401
|
+
# # find all sci-fi and fantasy books
|
|
402
|
+
# Dupe.find(:books) {|b| b.genre == 'sci-fi' or b.genre == 'fantasy'}
|
|
403
|
+
#
|
|
404
|
+
# # find all books written by people named 'Arthur'
|
|
405
|
+
# Dupe.find(:books) {|b| b.author.name.match /Arthur/}
|
|
406
|
+
#
|
|
407
|
+
# Also, if you have the need to explicitly specify :all or :first instead of relying on specifying the singular v. plural
|
|
408
|
+
# version of your resource name (perhaps the singular and plural version of your resource are exactly the same):
|
|
409
|
+
#
|
|
410
|
+
# Dupe.find(:all, :deer) {|d| d.type == 'doe'}
|
|
411
|
+
# Dupe.find(:first, :deer) {|d| d.name == 'Bambi'}
|
|
412
|
+
def find(*args, &block) # yield: record
|
|
413
|
+
all_or_first, factory_name = args[-2], args[-1]
|
|
414
|
+
match = block ? block : proc {true}
|
|
415
|
+
all_or_first = ((factory_name.to_s.pluralize == factory_name.to_s) ? :all : :first) unless all_or_first
|
|
416
|
+
factory_name = factory_name.to_s.singularize.to_sym
|
|
417
|
+
verify_factory_exists factory_name
|
|
418
|
+
result = factories[factory_name].find_records_like match
|
|
419
|
+
all_or_first == :all ? result : result.first
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def get_factory(factory) #:nodoc:
|
|
423
|
+
setup_factory(factory)
|
|
424
|
+
@factories[factory]
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def flush(factory=nil, destroy_definitions=false) #:nodoc:
|
|
428
|
+
if factory and factories[factory]
|
|
429
|
+
factories[factory].flush(destroy_definitions)
|
|
430
|
+
else
|
|
431
|
+
factories.each {|factory_name, factory| factory.flush(destroy_definitions)}
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def factories #:nodoc:
|
|
436
|
+
@factories ||= {}
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
private
|
|
440
|
+
|
|
441
|
+
def setup_factory(factory)
|
|
442
|
+
factories[factory] = Dupe.new(factory) unless factories[factory]
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def reset(factory)
|
|
446
|
+
factories[factory].flush if factories[factory]
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def verify_factory_exists(factory_name)
|
|
450
|
+
raise "Dupe doesn't know about the '#{factory_name}' resource" unless factories[factory_name]
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def flush(destroy_definitions=false) #:nodoc:
|
|
455
|
+
@records = []
|
|
456
|
+
@sequence = Sequence.new
|
|
457
|
+
@attributes = {} if destroy_definitions
|
|
458
|
+
ActiveResource::HttpMock.reset_from_dupe!
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def stub_services_with(record_template, count=1, starting_value=1, sequence_attribute=nil) #:nodoc:
|
|
462
|
+
sequence_attribute ||= record_template.keys.first
|
|
463
|
+
records = stub_records(record_template, count, starting_value, sequence_attribute)
|
|
464
|
+
generate_services_for(records, true)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def initialize(factory) #:nodoc:
|
|
468
|
+
@factory_name = factory
|
|
469
|
+
@attributes = {}
|
|
470
|
+
@config = Configuration.new
|
|
471
|
+
@mocker = MockServiceResponse.new(@factory_name)
|
|
472
|
+
@records = []
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def method_missing(method_name, *args, &block) #:nodoc:
|
|
476
|
+
args = [nil] if args.empty?
|
|
477
|
+
args << block
|
|
478
|
+
define_attribute(method_name.to_sym, *args)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def generate_services_for(records, records_already_processed=false) #:nodoc:
|
|
482
|
+
records = process_records records unless records_already_processed
|
|
483
|
+
@mocker.run_mocks(@records, @config.config[:record_identifiers])
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def find_records_like(match) #:nodoc:
|
|
487
|
+
@records.select {|r| match.call Record.new(r)}
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
private
|
|
491
|
+
def define_attribute(name, default_value=nil, prock=nil)
|
|
492
|
+
@attributes[name] = Attribute.new(name, default_value, prock)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def process_records(records)
|
|
496
|
+
records.map {|r| generate_record({:id => sequence}.merge(r))}
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def generate_record(overrides={})
|
|
500
|
+
define_missing_attributes(overrides.keys)
|
|
501
|
+
record = {}
|
|
502
|
+
@attributes.each do |attr_key, attr_class|
|
|
503
|
+
override_default_value = overrides[attr_key] || overrides[attr_key.to_s]
|
|
504
|
+
record[attr_key] = attr_class.value(override_default_value)
|
|
505
|
+
end
|
|
506
|
+
@records << record
|
|
507
|
+
record
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def sequence
|
|
511
|
+
(@sequence ||= Sequence.new).next
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def define_missing_attributes(keys)
|
|
515
|
+
keys.each {|k| define_attribute(k.to_sym) unless @attributes[k.to_sym]}
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def stub_records(record_template, count, starting_value, sequence_attribute)
|
|
519
|
+
overrides = record_template.merge({sequence_attribute => (record_template[sequence_attribute].to_s + starting_value.to_s), :id => sequence})
|
|
520
|
+
return [generate_record(overrides)] if count <= 1
|
|
521
|
+
[generate_record(overrides)] + stub_records(record_template, count-1, starting_value+1, sequence_attribute)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class Dupe
|
|
2
|
+
class MockServiceResponse #:nodoc:
|
|
3
|
+
attr_reader :mocks
|
|
4
|
+
attr_reader :resource_name
|
|
5
|
+
attr_reader :format
|
|
6
|
+
|
|
7
|
+
def initialize(resource_name, format=:xml)
|
|
8
|
+
@mocks = []
|
|
9
|
+
@resource_name = resource_name
|
|
10
|
+
@to_format = "to_#{format}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def define_mock(prock)
|
|
14
|
+
@mocks << prock
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def method_missing(method_name, *args, &block)
|
|
18
|
+
@mocks << block
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run_mocks(records, identifiers)
|
|
22
|
+
ActiveResource::HttpMock.respond_to do |mock|
|
|
23
|
+
@mocks.each do |a_mock|
|
|
24
|
+
a_mock.call mock, records
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
find_all(records)
|
|
28
|
+
records.each {|r| find_one(r, identifiers)}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
def find_all(records)
|
|
34
|
+
ActiveResource::HttpMock.respond_to do |mock|
|
|
35
|
+
mock.get "/#{@resource_name.to_s.pluralize}.xml", {}, format_for_service_response(records)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def find_one(record, identifiers)
|
|
40
|
+
ActiveResource::HttpMock.respond_to do |mock|
|
|
41
|
+
identifiers.each do |identifier|
|
|
42
|
+
mock.get "/#{@resource_name.to_s.pluralize}/#{record[identifier]}.xml", {}, format_for_service_response(record)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def format_for_service_response(records)
|
|
48
|
+
root = (records.is_a? Array) ? @resource_name.to_s.pluralize : @resource_name.to_s
|
|
49
|
+
@format == :json ? records.to_json({:root => root}): records.to_xml({:root => root})
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/dupe/record.rb
ADDED
data/lib/dupe.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'active_resource'
|
|
2
|
+
require 'active_resource/http_mock'
|
|
3
|
+
require 'dupe/dupe'
|
|
4
|
+
require 'dupe/sequence'
|
|
5
|
+
require 'dupe/mock_service_response'
|
|
6
|
+
require 'dupe/configuration'
|
|
7
|
+
require 'dupe/attribute'
|
|
8
|
+
require 'dupe/active_resource'
|
|
9
|
+
require 'dupe/cucumber_hooks'
|
|
10
|
+
require 'dupe/record'
|
|
11
|
+
|
|
12
|
+
path = defined?(RAILS_ROOT) ? RAILS_ROOT + '/features/dupe_definitions' : '../features/dupe_definitions'
|
|
13
|
+
if File.directory? path
|
|
14
|
+
Dir[File.join(path, '*.rb')].each do |file|
|
|
15
|
+
require file
|
|
16
|
+
end
|
|
17
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: moonmaster9000-dupe
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Matt Parker
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2009-09-14 00:00:00 -07:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: activeresource
|
|
17
|
+
type: :runtime
|
|
18
|
+
version_requirement:
|
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
20
|
+
requirements:
|
|
21
|
+
- - ">="
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: 2.3.3
|
|
24
|
+
version:
|
|
25
|
+
- !ruby/object:Gem::Dependency
|
|
26
|
+
name: cucumber
|
|
27
|
+
type: :runtime
|
|
28
|
+
version_requirement:
|
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 0.3.98
|
|
34
|
+
version:
|
|
35
|
+
description: Dupe rides on top of ActiveResource to allow you to cuke the client side of a service-oriented app without having to worry about whether or not the service is live or available while cuking.
|
|
36
|
+
email: moonmaster9000@gmail.com
|
|
37
|
+
executables: []
|
|
38
|
+
|
|
39
|
+
extensions: []
|
|
40
|
+
|
|
41
|
+
extra_rdoc_files:
|
|
42
|
+
- README.rdoc
|
|
43
|
+
files:
|
|
44
|
+
- README.rdoc
|
|
45
|
+
- lib/dupe.rb
|
|
46
|
+
- lib/dupe/active_resource.rb
|
|
47
|
+
- lib/dupe/attribute.rb
|
|
48
|
+
- lib/dupe/configuration.rb
|
|
49
|
+
- lib/dupe/cucumber_hooks.rb
|
|
50
|
+
- lib/dupe/dupe.rb
|
|
51
|
+
- lib/dupe/mock_service_response.rb
|
|
52
|
+
- lib/dupe/record.rb
|
|
53
|
+
- lib/dupe/sequence.rb
|
|
54
|
+
has_rdoc: false
|
|
55
|
+
homepage: http://github.com/moonmaster9000/dupe
|
|
56
|
+
post_install_message:
|
|
57
|
+
rdoc_options:
|
|
58
|
+
- --charset=UTF-8
|
|
59
|
+
require_paths:
|
|
60
|
+
- lib
|
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: "0"
|
|
66
|
+
version:
|
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: "0"
|
|
72
|
+
version:
|
|
73
|
+
requirements: []
|
|
74
|
+
|
|
75
|
+
rubyforge_project:
|
|
76
|
+
rubygems_version: 1.2.0
|
|
77
|
+
signing_key:
|
|
78
|
+
specification_version: 3
|
|
79
|
+
summary: A tool that helps you mock services while cuking.
|
|
80
|
+
test_files: []
|
|
81
|
+
|