dupe 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,188 @@
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
+
7
+ == Motivation
8
+
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
+
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
+
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.
20
+
21
+
22
+ == Installation
23
+
24
+ Add this to your cucumber.rb environment (config/environments/cucumber.rb)
25
+
26
+ config.gem 'dupe', :lib => 'dupe', :version => '>=0.3.5' :source => 'http://gemcutter.org'
27
+
28
+ Then run this rake task to install the gem:
29
+
30
+ # rake gems:install RAILS_ENV=cucumber
31
+
32
+ Lastly, from your rails project root, run:
33
+
34
+ # script/generate dupe
35
+
36
+
37
+ == Example
38
+
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>:
41
+
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.
46
+
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"
52
+
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
+
55
+ class Book < ActiveResource::Base
56
+ self.site = 'http://bookservice.domain'
57
+ end
58
+
59
+ class Author < ActiveResource::Base
60
+ self.site = 'http://bookservice.domain'
61
+ end
62
+
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/):
64
+
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
70
+
71
+ and the following cucumber step definitions (utilizing Dupe.create):
72
+
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.
177
+
178
+
179
+ == More
180
+
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/
183
+
184
+
185
+ == TODO List
186
+
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.
@@ -0,0 +1,135 @@
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
+
11
+ def delete_mock(http_method, path)
12
+ responses.reject! {|r| r[0].path == path && r[0].method == http_method}
13
+ end
14
+ end
15
+
16
+ # makes it possible to override existing request/response definitions
17
+ module ActiveResource #:nodoc:
18
+ class HttpMock #:nodoc:
19
+ class Responder #:nodoc:
20
+ for method in [ :post, :put, :get, :delete, :head ]
21
+ module_eval <<-EOE, __FILE__, __LINE__
22
+ def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
23
+ @responses.reject! {|r| r[0].path == path && r[0].method == :#{method}}
24
+ @responses << [Request.new(:#{method}, path, nil, request_headers), Response.new(body || "", status, response_headers)]
25
+ end
26
+ EOE
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ module CustomMocks
33
+ class NotImplementedError < StandardError
34
+ end
35
+
36
+ def custom_service(url)
37
+ raise NotImplementedError.new("you must implement the CustomMocks::custom_service method.")
38
+ end
39
+ end
40
+
41
+ module ActiveResource
42
+ class Connection #:nodoc:
43
+
44
+ include CustomMocks
45
+
46
+ class << self
47
+ attr_reader :request_log
48
+
49
+ def log_request(method, path, headers, response)
50
+ @request_log ||= []
51
+ @request_log << {
52
+ :method => method,
53
+ :path => path,
54
+ :headers => headers,
55
+ :response => response
56
+ }
57
+ end
58
+
59
+ def flush_request_log
60
+ @request_log = []
61
+ end
62
+
63
+ def print_request_log
64
+ @request_log ||= []
65
+ if @request_log.empty?
66
+ puts("\n -----No request attempts logged for this scenario")
67
+ return
68
+ end
69
+ puts "\n Request attempts logged for this scenario:\n --------------------------------------------\n\n"
70
+ @request_log.each do |request|
71
+ puts " Request: #{request[:method].to_s.upcase} #{request[:path]}"
72
+ puts " Headers: #{request[:headers].inspect}"
73
+ puts " Response Body:\n#{request[:response].body.split("\n").map {|s| (" "*6) + s}.join("\n")}"
74
+ puts " Response Code: #{request[:response].code}"
75
+ puts " Response Headers: #{request[:response].headers}"
76
+ puts " Response Message: #{request[:response].message}"
77
+ puts "\n\n"
78
+ end
79
+ end
80
+ end
81
+
82
+ # Execute a GET request.
83
+ # Used to get (find) resources.
84
+ def get(path, headers = {})
85
+ begin
86
+ response = request(:get, path, build_request_headers(headers, :get))
87
+
88
+ # if the request threw an exception
89
+ rescue
90
+ # make sure that it's not a custom service that should be handled on the fly
91
+ response_body = custom_service(path)
92
+ response_body = response_body.to_xml(:root => path.match(/^\/([^\/\.]+)/)[1]) if response_body.is_a?(Hash) || response_body.is_a?(Array)
93
+ ActiveResource::HttpMock.respond_to do |mock|
94
+ mock.get path, {}, response_body
95
+ end
96
+ response = request(:get, path, build_request_headers(headers, :get))
97
+ ActiveResource::HttpMock.delete_mock(:get, path)
98
+ end
99
+ ActiveResource::Connection.log_request(:get, path, build_request_headers(headers, :get), response)
100
+ format.decode(response.body)
101
+ end
102
+
103
+ # Execute a DELETE request (see HTTP protocol documentation if unfamiliar).
104
+ # Used to delete resources.
105
+ def delete(path, headers = {})
106
+ response = request(:delete, path, build_request_headers(headers, :delete))
107
+ ActiveResource::Connection.log_request(:delete, path, build_request_headers(headers, :delete), response)
108
+ response
109
+ end
110
+
111
+ # Execute a PUT request (see HTTP protocol documentation if unfamiliar).
112
+ # Used to update resources.
113
+ def put(path, body = '', headers = {})
114
+ response = request(:put, path, body.to_s, build_request_headers(headers, :put))
115
+ ActiveResource::Connection.log_request(:put, path, build_request_headers(headers, :put), response)
116
+ response
117
+ end
118
+
119
+ # Execute a POST request.
120
+ # Used to create new resources.
121
+ def post(path, body = '', headers = {})
122
+ response = request(:post, path, body.to_s, build_request_headers(headers, :post))
123
+ ActiveResource::Connection.log_request(:post, path, build_request_headers(headers, :post), response)
124
+ response
125
+ end
126
+
127
+ # Execute a HEAD request.
128
+ # Used to obtain meta-information about resources, such as whether they exist and their size (via response headers).
129
+ def head(path, headers = {})
130
+ response = request(:head, path, build_request_headers(headers))
131
+ ActiveResource::Connection.log_request(:head, path, build_request_headers(headers), response)
132
+ response
133
+ end
134
+ end
135
+ end
@@ -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,20 @@
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, method_name.to_s.plural? ? args : args.first)
12
+ end
13
+
14
+
15
+ private
16
+ def set_config_option(key, value)
17
+ @config[key] = value
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,8 @@
1
+ After do |scenario| #:nodoc:
2
+ if Dupe.global_configuration.config[:debug] == true
3
+ ActiveResource::Connection.print_request_log
4
+ ActiveResource::Connection.flush_request_log
5
+ end
6
+
7
+ Dupe.flush
8
+ end
data/lib/dupe/dupe.rb ADDED
@@ -0,0 +1,477 @@
1
+ # Author:: Matt Parker (mailto:moonmaster9000@gmail.com)
2
+ # License:: Distributes under the same terms as Ruby
3
+
4
+ class Dupe
5
+ attr_reader :factory_name #:nodoc:
6
+ attr_reader :configuration #:nodoc:
7
+ attr_reader :attributes #:nodoc:
8
+ attr_reader :config #:nodoc:
9
+ attr_reader :mocker #:nodoc:
10
+ attr_reader :records #:nodoc:
11
+
12
+ class << self
13
+ attr_accessor :factories #:nodoc:
14
+ attr_accessor :global_configuration #:nodoc:
15
+
16
+ # Create data definitions for your resources. This allows you to setup default values for columns
17
+ # and even provide data transformations.
18
+ #
19
+ # For example, suppose you had the following cucumber scenario:
20
+ #
21
+ # # RAILS_ROOT/features/library/find_book.feature
22
+ # Feature: Find a book
23
+ # As a reader
24
+ # I want to find books in my library
25
+ # So that I can read them
26
+ #
27
+ # Scenario: Browsing books
28
+ # Given the following author:
29
+ # | name | date_of_birth |
30
+ # | Arthur C. Clarke | 1917-12-16 |
31
+ #
32
+ # And the following book:
33
+ # | name | author |
34
+ # | 2001: A Space Odyssey | Arthur C. Clarke |
35
+ #
36
+ # When....
37
+ #
38
+ #
39
+ # We can use Dupe.define to
40
+ # * Transform data (e.g., turn the string '1917-12-16' into a Date object)
41
+ # * Provide default values for attributes (e.g., give all author's a default biography)
42
+ # * Associate records (e.g., given an author name, return the author record associated with that name)
43
+ #
44
+ # To accomplish the afore mentioned definitions:
45
+ #
46
+ # # RAILS_ROOT/features/dupe_definitions/book.rb
47
+ #
48
+ # Dupe.define :author do |define|
49
+ # define.bio 'Lorem ipsum delor.'
50
+ # define.date_of_birth do |d|
51
+ # Date.parse(t)
52
+ # end
53
+ # end
54
+ #
55
+ # Dupe.define :book do |define|
56
+ # define.author do |author_name|
57
+ # Dupe.find(:author) {|a| a.name == author_name}
58
+ # end
59
+ # end
60
+ #
61
+ # -----------------------------------------------------------------------------------------------------------------
62
+ #
63
+ # # RAILS_ROOT/features/step_definitions/library/find_book_steps.rb
64
+ #
65
+ # Given /^the following author:$/ do |author_table|
66
+ # Dupe.create(:author, author_table.hashes)
67
+ # end
68
+ #
69
+ # Given /^the following book:$/ do |book_table|
70
+ # Dupe.create(:book, book_table.hashes)
71
+ # end
72
+ #
73
+ # When cucumber encounters the "Given the following author:" line, the corresponding step definition
74
+ # will ask Dupe to mock ActiveResource responses to find(:all) and find(:id) with the data
75
+ # specified in the cucumber hash table immediately following the "Given the following author:" line.
76
+ # Since we didn't specify a 'bio' value in our cucumber hash table, Dupe will give it the
77
+ # default value 'Lorem ipsum delor.'. Also, it will transform the 'date_of_birth' value we provided in the hash
78
+ # table into a time object.
79
+ #
80
+ # Similarly, for the :book cucumber hash table, Dupe will transform the author name we provided
81
+ # into the author object we had already specified in the :author table.
82
+ #
83
+ # In terms of mocked responses, we could expect something like:
84
+ #
85
+ # # Author.find(1) --> GET /authors/1.xml
86
+ # <?xml version="1.0" encoding="UTF-8"?>
87
+ # <author>
88
+ # <id type="integer">1</id>
89
+ # <name>Arthur C. Clarke</name>
90
+ # <bio>Lorem ipsum delor.</bio>
91
+ # <date_of_birth>1917-12-16T00:00:00Z</date_of_birth>
92
+ # </author>
93
+ #
94
+ # # Book.find(1) --> GET /books/1.xml
95
+ # <?xml version="1.0" encoding="UTF-8"?>
96
+ # <book>
97
+ # <id type="integer">1</id>
98
+ # <name>2001: A Space Odyssey</name>
99
+ # <author>
100
+ # <id type="integer">1</id>
101
+ # <name>Arthur C. Clarke</name>
102
+ # <bio>Lorem ipsum delor.</bio>
103
+ # <date_of_birth>1917-12-16T00:00:00Z</date_of_birth>
104
+ # </author>
105
+ # </book>
106
+ def define(factory) # yield: define
107
+ setup_factory(factory)
108
+ yield @factories[factory]
109
+ end
110
+
111
+ # This method will cause Dupe to mock resources for the record(s) provided.
112
+ # The "records" value may be either a hash or an array of hashes.
113
+ # For example, suppose you'd like to mock a single author ActiveResource object:
114
+ #
115
+ # Dupe.create :author, :name => 'Arthur C. Clarke'
116
+ #
117
+ # This will translate into the following two mocked resource calls:
118
+ #
119
+ # # Author.find(:all) --> GET /authors.xml
120
+ # <?xml version="1.0" encoding="UTF-8"?>
121
+ # <authors>
122
+ # <author>
123
+ # <id type="integer">1</id>
124
+ # <name>Arthur C. Clarke</name>
125
+ # </author>
126
+ # </authors>
127
+ #
128
+ # # Author.find(1) --> GET /authors/1.xml
129
+ # <?xml version="1.0" encoding="UTF-8"?>
130
+ # <author>
131
+ # <id type="integer">1</id>
132
+ # <name>Arthur C. Clarke</name>
133
+ # </author>
134
+ #
135
+ # However, suppose you wanted to mock two or more authors.
136
+ #
137
+ # Dupe.create :author, [{:name => 'Arthur C. Clarke'}, {:name => 'Robert Heinlein'}]
138
+ #
139
+ # This will translate into the following three mocked resource calls:
140
+ #
141
+ # # Author.find(:all) --> GET /authors.xml
142
+ # <?xml version="1.0" encoding="UTF-8"?>
143
+ # <authors>
144
+ # <author>
145
+ # <id type="integer">1</id>
146
+ # <name>Arthur C. Clarke</name>
147
+ # </author>
148
+ # <author>
149
+ # <id type="integer">2</id>
150
+ # <name>Robert Heinlein</name>
151
+ # </author>
152
+ # </authors>
153
+ #
154
+ # # Author.find(1) --> GET /authors/1.xml
155
+ # <?xml version="1.0" encoding="UTF-8"?>
156
+ # <author>
157
+ # <id type="integer">1</id>
158
+ # <name>Arthur C. Clarke</name>
159
+ # </author>
160
+ #
161
+ # # Author.find(2) --> GET /authors/2.xml
162
+ # <?xml version="1.0" encoding="UTF-8"?>
163
+ # <author>
164
+ # <id type="integer">2</id>
165
+ # <name>Robert Heinlein</name>
166
+ # </author>
167
+ def create(factory, records={})
168
+ setup_factory(factory)
169
+ raise Exception, "unknown records type" if !records.nil? and !records.is_a?(Array) and !records.is_a?(Hash)
170
+ records = [records] if records.is_a?(Hash)
171
+ @factories[factory].generate_services_for(records)
172
+ end
173
+
174
+ # You can use this method to quickly stub out a large number of resources. For example:
175
+ #
176
+ # Dupe.stub 20, :authors
177
+ #
178
+ #
179
+ # Assuming you had an :author resource definition like:
180
+ #
181
+ # Dupe.define :author {|author| author.name('default')}
182
+ #
183
+ #
184
+ # then stub would have generated 20 author records like:
185
+ #
186
+ # {:name => 'default', :id => 1}
187
+ # ....
188
+ # {:name => 'default', :id => 20}
189
+ #
190
+ # and it would also have mocked find(id) and find(:all) responses for these records (along with any other custom mocks you've
191
+ # setup via Dupe.define_mocks). (Had you not defined an author resource, then the stub would have generated 20 author records
192
+ # where the only attribute is the id).
193
+ #
194
+ # Of course, it's more likely that you wanted to dupe 20 <em>different</em> authors. You can accomplish this by simply doing:
195
+ #
196
+ # Dupe.stub 20, :authors, :like => {:name => proc {|n| "author #{n}"}}
197
+ #
198
+ # which would generate 20 author records like:
199
+ #
200
+ # {:name => 'author 1', :id => 1}
201
+ # ....
202
+ # {:name => 'author 20', :id => 20}
203
+ #
204
+ # You may also override the sequence starting value:
205
+ #
206
+ # Dupe.stub 20, :authors, :like => {:name => proc {|n| "author #{n}"}}, :starting_with => 150
207
+ #
208
+ # This would generate 20 author records like:
209
+ #
210
+ # {:name => 'author 150', :id => 1}
211
+ # ....
212
+ # {:name => 'author 169', :id => 20}
213
+ #
214
+ # Naturally, stub will consult the Dupe.define definitions for anything it's attempting to stub
215
+ # and will honor those definitions (default values, transformations) as you would expect.
216
+ def stub(count, factory, options={})
217
+ factory = factory.to_s.singularize.to_sym
218
+ setup_factory(factory)
219
+ @factories[factory].stub_services_with((options[:like] || {}), count.to_i, (options[:starting_with] || 1))
220
+ end
221
+
222
+ # === Global Configuration
223
+ #
224
+ # On a global level, configure supports the following options (expect this list to grow as the app grows):
225
+ # debug: list the attempted requests and mocked responses that happened during the course of a scenario
226
+ #
227
+ # To turn on debugging, simply do:
228
+ # Dupe.configure do |global_config|
229
+ # global_config.debug true
230
+ # end
231
+ #
232
+ # === Factory Configuration
233
+ #
234
+ # On a factory level, configure support the following options (expect this list to grow as the app grows):
235
+ # record_identifiers: a list of attributes that are unique to each record in that resource factory.
236
+ #
237
+ # The "record_identifiers" configuration option allows you to override the array record identifiers for your resources ([:id], by default)
238
+ #
239
+ # For example, suppose the RESTful application your trying to consume supports lookups by both a textual 'label'
240
+ # and a numeric 'id', and that it contains an author service where the author with id '1' has the label 'arthur-c-clarke'.
241
+ # 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>.
242
+ #
243
+ # Thus, to ensure that Dupe mocks both, do the following:
244
+ # Dupe.configure :author do |configure|
245
+ # configure.record_identifiers :id, :label
246
+ # end
247
+ #
248
+ # With this configuration, a <tt>Dupe.create :author, :name => 'Arthur C. Clarke', :label => 'arthur-c-clarke'</tt>
249
+ # will result in the following mocked service calls:
250
+ #
251
+ # <tt>Author.find(1) --> (GET /authors/1.xml)</tt>
252
+ #
253
+ # <?xml version="1.0" encoding="UTF-8"?>
254
+ # <author>
255
+ # <id type="integer">1</id>
256
+ # <name>Arthur C. Clarke</name>
257
+ # <label>arthur-c-clarke</label>
258
+ # </author>
259
+ #
260
+ #
261
+ # <tt>Author.find('arthur-c-clarke') --> (GET /authors/arthur-c-clarke.xml)</tt>
262
+ #
263
+ # <?xml version="1.0" encoding="UTF-8"?>
264
+ # <author>
265
+ # <id type="integer">1</id>
266
+ # <name>Arthur C. Clarke</name>
267
+ # <label>arthur-c-clarke</label>
268
+ # </author>
269
+ def configure(factory=nil) # yield: configure
270
+ yield global_configuration and return unless factory
271
+ setup_factory(factory)
272
+ yield @factories[factory].config
273
+ end
274
+
275
+ # By default, Dupe will mock responses to ActiveResource <tt>find(:all)</tt> and <tt>find(id)</tt>.
276
+ # However, it's likely that your cucumber scenarios will eventually fire off an ActiveResource request that's
277
+ # something other than these basic lookups.
278
+ #
279
+ # Dupe.define_mocks allows you to add new resource mocks and override the built-in resource mocks.
280
+ #
281
+ # For example, suppose you had a Book ActiveResource model, and you want to use it to get the :count of all
282
+ # Books in the back end system your consuming. <tt>Book.get(:count)</tt> would fire off an HTTP request to the
283
+ # backend service like <tt>"GET /books/count.xml"</tt>, and assuming the service is set up to respond to that
284
+ # request, you might expect to get something back like:
285
+ #
286
+ # <?xml version="1.0" encoding="UTF-8"?>
287
+ # <hash>
288
+ # <count type="integer">3</count>
289
+ # </hash>
290
+ #
291
+ # To mock this for the purposes of cuking, you could do the following:
292
+ #
293
+ # Dupe.define_mocks :book do |define|
294
+ # define.count do |mock, records|
295
+ # mock.get "/books/count.xml", {}, {:count => records.size}.to_xml
296
+ # end
297
+ # end
298
+ #
299
+ # The <tt>mock</tt> object is the ActiveResource::HttpMock object. Please see the documentation for that
300
+ # if you would like to know more about what's possible with it.
301
+ def define_mocks(factory) # yield: define
302
+ setup_factory(factory)
303
+ yield @factories[factory].mocker
304
+ end
305
+
306
+
307
+ # Search for a resource. This works a bit differently from both ActiveRecord's find and ActiveResource's find.
308
+ # This is most often used for defining associations between objects (Dupe.define).
309
+ # It will return a hash representation of the resource (or an array of hashes if we asked for multiple records).
310
+ #
311
+ # For example, suppose we have an author resource, and a book resource with a nested author attribute (in ActiveRecord
312
+ # parlance, Book belongs_to Author, Author has_many Book).
313
+ #
314
+ # Now suppose we've created the following cucumber scenario:
315
+ #
316
+ # Scenario: Browsing books
317
+ # Given the following author:
318
+ # | name | date_of_birth |
319
+ # | Arthur C. Clarke | 1917-12-16 |
320
+ #
321
+ # And the following books:
322
+ # | name | author | published | genre |
323
+ # | 2001: A Space Odyssey | Arthur C. Clarke | 1968 | sci-fi |
324
+ # | A Fall of Moondust | Arthur C. Clarke | 1961 | fantasy |
325
+ # | Rendezvous with Rama | Arthur C. Clarke | 1972 | sci-fi |
326
+ #
327
+ # When....
328
+ #
329
+ # To link up the book and author, we could create the following book definition
330
+ #
331
+ # Dupe.define :book do |book|
332
+ # book.author {|name| Dupe.find(:author) {|a| a.name == name}}
333
+ # end
334
+ #
335
+ # The line <tt>Dupe.find(:author) {|a| a.name == name}</tt> could be translated as
336
+ # "find the first author record where the author's name equals `name`".
337
+ #
338
+ # Dupe decided to return only a single record because we specified <tt>find(:author)</tt>.
339
+ # Had we instead specified <tt>find(:authors)</tt>, resource factory would have instead returned an array of results.
340
+ #
341
+ # More examples:
342
+ #
343
+ # # find all books written in the 1960's
344
+ # Dupe.find(:books) {|b| b.published >= 1960 and b.published <= 1969}
345
+ #
346
+ # # return the first book found that was written by Arthur C. Clarke (nested resources example)
347
+ # Dupe.find(:book) {|b| b.author.name == 'Arthur C. Clarke'}
348
+ #
349
+ # # find all sci-fi and fantasy books
350
+ # Dupe.find(:books) {|b| b.genre == 'sci-fi' or b.genre == 'fantasy'}
351
+ #
352
+ # # find all books written by people named 'Arthur'
353
+ # Dupe.find(:books) {|b| b.author.name.match /Arthur/}
354
+ #
355
+ # Also, if you have the need to explicitly specify :all or :first instead of relying on specifying the singular v. plural
356
+ # version of your resource name (perhaps the singular and plural version of your resource are exactly the same):
357
+ #
358
+ # Dupe.find(:all, :deer) {|d| d.type == 'doe'}
359
+ # Dupe.find(:first, :deer) {|d| d.name == 'Bambi'}
360
+ def find(*args, &block) # yield: record
361
+ all_or_first, factory_name = args[-2], args[-1]
362
+ match = block ? block : proc {true}
363
+ all_or_first = ((factory_name.to_s.plural?) ? :all : :first) unless all_or_first
364
+ factory_name = factory_name.to_s.singularize.to_sym
365
+ verify_factory_exists factory_name
366
+ result = factories[factory_name].find_records_like match
367
+ all_or_first == :all ? result : result.first
368
+ end
369
+
370
+ def get_factory(factory) #:nodoc:
371
+ setup_factory(factory)
372
+ @factories[factory]
373
+ end
374
+
375
+ def flush(factory=nil, destroy_definitions=false) #:nodoc:
376
+ if factory and factories[factory]
377
+ factories[factory].flush(destroy_definitions)
378
+ else
379
+ factories.each {|factory_name, factory| factory.flush(destroy_definitions)}
380
+ end
381
+ end
382
+
383
+ def factories #:nodoc:
384
+ @factories ||= {}
385
+ end
386
+
387
+ def global_configuration #:nodoc:
388
+ @global_configuration ||= Configuration.new
389
+ end
390
+
391
+ private
392
+
393
+ def setup_factory(factory)
394
+ factories[factory] = Dupe.new(factory) unless factories[factory]
395
+ end
396
+
397
+ def reset(factory)
398
+ factories[factory].flush if factories[factory]
399
+ end
400
+
401
+ def verify_factory_exists(factory_name)
402
+ raise "Dupe doesn't know about the '#{factory_name}' resource" unless factories[factory_name]
403
+ end
404
+ end
405
+
406
+ def flush(destroy_definitions=false) #:nodoc:
407
+ @records = []
408
+ @sequence = Sequence.new
409
+ @attributes = {} if destroy_definitions
410
+ ActiveResource::HttpMock.reset_from_dupe!
411
+ end
412
+
413
+ def stub_services_with(record_template, count=1, starting_value=1) #:nodoc:
414
+ records = stub_records(record_template, count, starting_value)
415
+ generate_services_for(records, true)
416
+ end
417
+
418
+ def initialize(factory) #:nodoc:
419
+ @factory_name = factory
420
+ @attributes = {}
421
+ @config = Configuration.new
422
+ @mocker = MockServiceResponse.new(@factory_name)
423
+ @records = []
424
+ end
425
+
426
+ def method_missing(method_name, *args, &block) #:nodoc:
427
+ args = [nil] if args.empty?
428
+ args << block
429
+ define_attribute(method_name.to_sym, *args)
430
+ end
431
+
432
+ def generate_services_for(records, records_already_processed=false) #:nodoc:
433
+ records = process_records records unless records_already_processed
434
+ @mocker.run_mocks(@records, @config.config[:record_identifiers])
435
+ end
436
+
437
+ def find_records_like(match) #:nodoc:
438
+ @records.select {|r| match.call Record.new(r)}
439
+ end
440
+
441
+ private
442
+ def define_attribute(name, default_value=nil, prock=nil)
443
+ @attributes[name] = Attribute.new(name, default_value, prock)
444
+ end
445
+
446
+ def process_records(records)
447
+ records.map {|r| generate_record({:id => sequence}.merge(r))}
448
+ end
449
+
450
+ def generate_record(overrides={})
451
+ define_missing_attributes(overrides.keys)
452
+ record = {}
453
+ @attributes.each do |attr_key, attr_class|
454
+ override_default_value = overrides[attr_key] || overrides[attr_key.to_s]
455
+ record[attr_key] = attr_class.value(override_default_value)
456
+ end
457
+ @records << record
458
+ record
459
+ end
460
+
461
+ def sequence
462
+ (@sequence ||= Sequence.new).next
463
+ end
464
+
465
+ def define_missing_attributes(keys)
466
+ keys.each {|k| define_attribute(k.to_sym) unless @attributes[k.to_sym]}
467
+ end
468
+
469
+ def stub_records(record_template, count, stub_number)
470
+ overrides = record_template.merge({})
471
+ overrides.keys.each {|k| overrides[k] = overrides[k].call(stub_number) if overrides[k].respond_to?(:call)}
472
+ overrides = {:id => sequence}.merge(overrides) unless overrides[:id]
473
+ return [generate_record(overrides)] if count <= 1
474
+ [generate_record(overrides)] + stub_records(record_template, count-1, stub_number+1)
475
+ end
476
+
477
+ end
@@ -0,0 +1,55 @@
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
+ private
32
+ def find_all(records)
33
+ ActiveResource::HttpMock.respond_to do |mock|
34
+ mock.get "#{prefix}#{@resource_name.to_s.pluralize}.xml", {}, format_for_service_response(records)
35
+ end
36
+ end
37
+
38
+ def find_one(record, identifiers)
39
+ ActiveResource::HttpMock.respond_to do |mock|
40
+ identifiers.each do |identifier|
41
+ mock.get "#{prefix}#{@resource_name.to_s.pluralize}/#{record[identifier]}.xml", {}, format_for_service_response(record)
42
+ end
43
+ end
44
+ end
45
+
46
+ def format_for_service_response(records)
47
+ root = (records.is_a? Array) ? @resource_name.to_s.pluralize : @resource_name.to_s
48
+ @format == :json ? records.to_json({:root => root}): records.to_xml({:root => root})
49
+ end
50
+
51
+ def prefix
52
+ @resource_name.to_s.camelize.constantize.prefix.gsub(/\/+/, '/') rescue "/"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,19 @@
1
+ class Dupe
2
+ class Record #:nodoc:
3
+ def initialize(hash)
4
+ @attributes = hash.merge(hash) do |k,v|
5
+ if v.is_a?(Hash)
6
+ Record.new(v)
7
+ elsif v.is_a?(Array)
8
+ v.map {|r| Record.new(r)}
9
+ else
10
+ v
11
+ end
12
+ end
13
+ end
14
+
15
+ def method_missing(method_name, *args, &block)
16
+ @attributes[method_name.to_sym]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ class Dupe
2
+ class Sequence #:nodoc:
3
+ def initialize(start=0)
4
+ @sequence_value = start
5
+ end
6
+
7
+ def next
8
+ @sequence_value += 1
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ class String
2
+ def plural?
3
+ self.to_s == pluralize
4
+ end
5
+
6
+ def singular?
7
+ self.to_s == singularize
8
+ end
9
+ end
data/lib/dupe.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'active_resource'
2
+ require 'active_resource/http_mock'
3
+ require 'dupe/string'
4
+ require 'dupe/dupe'
5
+ require 'dupe/sequence'
6
+ require 'dupe/mock_service_response'
7
+ require 'dupe/configuration'
8
+ require 'dupe/attribute'
9
+ require 'dupe/active_resource'
10
+ require 'dupe/cucumber_hooks'
11
+ require 'dupe/record'
12
+
13
+ path = defined?(RAILS_ROOT) ? RAILS_ROOT + '/features/dupe_definitions' : '../features/dupe_definitions'
14
+ if File.directory? path
15
+ Dir[File.join(path, '*.rb')].each do |file|
16
+ require file
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ class DupeGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ # make sure the features and features/support directories exist
5
+ m.directory 'features/support'
6
+
7
+ # copy the custom_mocks.rb example file into features/support
8
+ m.template 'custom_mocks.rb', 'features/support/custom_mocks.rb'
9
+
10
+ # copy the dupe_setup.rb example file into features/support
11
+ m.template 'dupe_setup.rb', 'features/support/dupe_setup.rb'
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,34 @@
1
+ module CustomMocks
2
+ # Maps a service request url to a Dupe find. By default, Dupe will only
3
+ # mock simple requests like SomeResource.find(some_id) or SomeResource.find(:all)
4
+ #
5
+ # For example, suppose you have a Book < ActiveResource::Base class, and
6
+ # somewhere your code does:
7
+ #
8
+ # Book.find :all, :params => {:limit => 10, :offset => 20}
9
+ #
10
+ # That in turn will send off a request to a url like:
11
+ #
12
+ # /books.xml?limit=10&offset=20
13
+ #
14
+ # In this file, you could add a "when" statement like:
15
+ #
16
+ # when %r{/books.xml\?limit=(\d+)&offset=(\d+)$}
17
+ # start = $2.to_i
18
+ # finish = start + $1.to_i - 1
19
+ # Dupe.find(:books)[start..finish]
20
+ def custom_service(url)
21
+ case url
22
+
23
+ # remove this and replace it with a real custom mock
24
+ when %r{/bogus_url}
25
+ ''
26
+
27
+ else
28
+ raise StandardError.new(
29
+ "There is no custom service mapping for \"#{url}\"." +
30
+ "Now go to features/support/custom_mocks.rb and add it."
31
+ )
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,23 @@
1
+ Dupe.configure do |global_config|
2
+ # set this to false if you don't want to see the mocked
3
+ # xml output after each scenario
4
+ global_config.debug true
5
+ end
6
+
7
+ # You can also place your resource definitions in this file.
8
+ =begin
9
+ # Example resource definition
10
+ Dupe.define :books do |book|
11
+ book.name 'default name'
12
+
13
+ # supporting Dupe.create :book, :genre => 'Sci-fi'
14
+ book.genre do |genre_name|
15
+ Dupe.find(:genre) {|g| g.name == genre_name}
16
+ end
17
+
18
+ # supporting Dupe.create :book, :authors => 'Arthur C. Clarke, Gentry Lee'
19
+ book.authors do |author_names|
20
+ Dupe.find(:authors) {|a| author_names.split(/,\ */).include?(a.name)}
21
+ end
22
+ end
23
+ =end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dupe
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.5
5
+ platform: ruby
6
+ authors:
7
+ - Matt Parker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-28 00:00:00 -04: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
+ description: |-
26
+ Dupe rides on top of ActiveResource to allow you to cuke the client side of
27
+ a service-oriented app without having to worry about whether or not the service
28
+ is live or available while cuking.
29
+ email: moonmaster9000@gmail.com
30
+ executables: []
31
+
32
+ extensions: []
33
+
34
+ extra_rdoc_files:
35
+ - README.rdoc
36
+ files:
37
+ - README.rdoc
38
+ - lib/dupe.rb
39
+ - lib/dupe/active_resource.rb
40
+ - lib/dupe/attribute.rb
41
+ - lib/dupe/configuration.rb
42
+ - lib/dupe/cucumber_hooks.rb
43
+ - lib/dupe/dupe.rb
44
+ - lib/dupe/mock_service_response.rb
45
+ - lib/dupe/record.rb
46
+ - lib/dupe/sequence.rb
47
+ - lib/dupe/string.rb
48
+ - rails_generators/dupe/dupe_generator.rb
49
+ - rails_generators/dupe/templates/custom_mocks.rb
50
+ - rails_generators/dupe/templates/dupe_setup.rb
51
+ has_rdoc: true
52
+ homepage: http://github.com/moonmaster9000/dupe
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options:
57
+ - --charset=UTF-8
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.3.5
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: A tool that helps you mock services while cuking.
79
+ test_files: []
80
+