dupe 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +188 -0
- data/lib/dupe/active_resource.rb +135 -0
- data/lib/dupe/attribute.rb +17 -0
- data/lib/dupe/configuration.rb +20 -0
- data/lib/dupe/cucumber_hooks.rb +8 -0
- data/lib/dupe/dupe.rb +477 -0
- data/lib/dupe/mock_service_response.rb +55 -0
- data/lib/dupe/record.rb +19 -0
- data/lib/dupe/sequence.rb +11 -0
- data/lib/dupe/string.rb +9 -0
- data/lib/dupe.rb +18 -0
- data/rails_generators/dupe/dupe_generator.rb +14 -0
- data/rails_generators/dupe/templates/custom_mocks.rb +34 -0
- data/rails_generators/dupe/templates/dupe_setup.rb +23 -0
- metadata +80 -0
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
|
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
|
data/lib/dupe/record.rb
ADDED
@@ -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
|
data/lib/dupe/string.rb
ADDED
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
|
+
|