harper 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rvmrc +2 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +62 -0
- data/Rakefile +21 -0
- data/bin/harper +7 -0
- data/features/mock_http_rpc_service.feature +67 -0
- data/features/step_definitions/http_steps.rb +34 -0
- data/features/step_definitions/mock_definition_steps.rb +26 -0
- data/features/step_definitions/web_steps.rb +219 -0
- data/features/support/client_application.rb +55 -0
- data/features/support/env.rb +25 -0
- data/features/support/paths.rb +29 -0
- data/harper.gemspec +33 -0
- data/lib/harper.rb +81 -0
- data/spec/harper_spec.rb +151 -0
- data/spec/spec_helper.rb +11 -0
- metadata +210 -0
data/.rvmrc
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
# source :gemcutter
|
6
|
+
|
7
|
+
# gem "rake", "0.8.7"
|
8
|
+
# gem "sinatra"
|
9
|
+
# gem "httparty"
|
10
|
+
# gem "json", "~> 1.4.6"
|
11
|
+
|
12
|
+
# group :test do
|
13
|
+
# gem "rack-test"
|
14
|
+
# gem "sham_rack"
|
15
|
+
# gem "rspec"
|
16
|
+
# gem "machinist"
|
17
|
+
# gem "faker"
|
18
|
+
# gem "cucumber", ">= 0.10"
|
19
|
+
# gem "cucumber-sinatra"
|
20
|
+
# end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
harper (0.0.1)
|
5
|
+
httparty
|
6
|
+
json (>= 1.4.6)
|
7
|
+
sinatra (>= 1.0.0)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: http://rubygems.org/
|
11
|
+
specs:
|
12
|
+
builder (3.0.0)
|
13
|
+
crack (0.1.8)
|
14
|
+
cucumber (0.10.3)
|
15
|
+
builder (>= 2.1.2)
|
16
|
+
diff-lcs (>= 1.1.2)
|
17
|
+
gherkin (>= 2.3.8)
|
18
|
+
json (>= 1.4.6)
|
19
|
+
term-ansicolor (>= 1.0.5)
|
20
|
+
cucumber-sinatra (0.3.1)
|
21
|
+
templater (>= 1.0.0)
|
22
|
+
diff-lcs (1.1.2)
|
23
|
+
extlib (0.9.15)
|
24
|
+
gherkin (2.3.9)
|
25
|
+
json (>= 1.4.6)
|
26
|
+
highline (1.6.2)
|
27
|
+
httparty (0.7.7)
|
28
|
+
crack (= 0.1.8)
|
29
|
+
json (1.4.6)
|
30
|
+
rack (1.3.0)
|
31
|
+
rack-test (0.6.0)
|
32
|
+
rack (>= 1.0)
|
33
|
+
rspec (2.6.0)
|
34
|
+
rspec-core (~> 2.6.0)
|
35
|
+
rspec-expectations (~> 2.6.0)
|
36
|
+
rspec-mocks (~> 2.6.0)
|
37
|
+
rspec-core (2.6.3)
|
38
|
+
rspec-expectations (2.6.0)
|
39
|
+
diff-lcs (~> 1.1.2)
|
40
|
+
rspec-mocks (2.6.0)
|
41
|
+
sham_rack (1.3.3)
|
42
|
+
rack
|
43
|
+
sinatra (1.2.6)
|
44
|
+
rack (~> 1.1)
|
45
|
+
tilt (>= 1.2.2, < 2.0)
|
46
|
+
templater (1.0.0)
|
47
|
+
diff-lcs (>= 1.1.2)
|
48
|
+
extlib (>= 0.9.5)
|
49
|
+
highline (>= 1.4.0)
|
50
|
+
term-ansicolor (1.0.5)
|
51
|
+
tilt (1.3.1)
|
52
|
+
|
53
|
+
PLATFORMS
|
54
|
+
ruby
|
55
|
+
|
56
|
+
DEPENDENCIES
|
57
|
+
cucumber (>= 0.10)
|
58
|
+
cucumber-sinatra
|
59
|
+
harper!
|
60
|
+
rack-test
|
61
|
+
rspec
|
62
|
+
sham_rack
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
require 'cucumber'
|
3
|
+
require 'cucumber/rake/task'
|
4
|
+
require 'bundler'
|
5
|
+
|
6
|
+
namespace :gem do
|
7
|
+
Bundler::GemHelper.install_tasks
|
8
|
+
end
|
9
|
+
|
10
|
+
namespace :test do
|
11
|
+
RSpec::Core::RakeTask.new(:rspec) do |t|
|
12
|
+
t.rspec_opts = "--color"
|
13
|
+
end
|
14
|
+
|
15
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
16
|
+
t.cucumber_opts = "features --format pretty"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "Run all tests"
|
21
|
+
task :test => ['test:rspec', 'test:features']
|
data/bin/harper
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
Feature: Mock an HTTP RPC web service
|
2
|
+
As a developer who cares about feedback cycles
|
3
|
+
I want to be able to easily mock service endpoints
|
4
|
+
So that I can fluently describe my test scenarios
|
5
|
+
|
6
|
+
Scenario: Submit a simple service request to mock
|
7
|
+
Given the following response mock, known as "original":
|
8
|
+
"""
|
9
|
+
{
|
10
|
+
"url": "/service",
|
11
|
+
"method": "GET",
|
12
|
+
"content-type": "text/plain",
|
13
|
+
"body": "fake body"
|
14
|
+
}
|
15
|
+
"""
|
16
|
+
When the application POSTs the mock "original" to "/h/mocks"
|
17
|
+
Then the response code should be "201"
|
18
|
+
And the "original" mock is available at the URL in the "Location" header
|
19
|
+
When the application issues a "GET" request for "/service"
|
20
|
+
Then the response code should be "200"
|
21
|
+
And the response "content-type" header should be "text/plain"
|
22
|
+
And the response body should be:
|
23
|
+
"""
|
24
|
+
fake body
|
25
|
+
"""
|
26
|
+
When the application removes the mock "original"
|
27
|
+
And the application issues a "GET" request for "/service"
|
28
|
+
Then the response code should be "503"
|
29
|
+
|
30
|
+
Scenario Outline: All HTTP methods can be mocked
|
31
|
+
Given a defined response mock with a "method" of "<METHOD>"
|
32
|
+
When the application issues a "<METHOD>" request to the mock
|
33
|
+
Then the response code should be "200"
|
34
|
+
Examples:
|
35
|
+
| METHOD |
|
36
|
+
| GET |
|
37
|
+
| POST |
|
38
|
+
| DELETE |
|
39
|
+
| PUT |
|
40
|
+
|
41
|
+
Scenario Outline: HTTP status codes can be mocked
|
42
|
+
Given a defined response mock with a "status" of "<STATUS>"
|
43
|
+
When the application issues a "GET" request to the mock
|
44
|
+
Then the response code should be "<STATUS>"
|
45
|
+
Examples:
|
46
|
+
| STATUS |
|
47
|
+
| 200 |
|
48
|
+
| 201 |
|
49
|
+
| 206 |
|
50
|
+
| 301 |
|
51
|
+
| 304 |
|
52
|
+
| 401 |
|
53
|
+
| 403 |
|
54
|
+
| 404 |
|
55
|
+
| 500 |
|
56
|
+
| 503 |
|
57
|
+
|
58
|
+
Scenario Outline: Responses from the server can be delayed
|
59
|
+
Given a defined response mock with a "delay" of "<MILLISECONDS>"
|
60
|
+
When the application issues a "GET" request to the mock
|
61
|
+
Then the response time should be at least "<MILLISECONDS>"
|
62
|
+
Examples:
|
63
|
+
| MILLISECONDS |
|
64
|
+
| 0 |
|
65
|
+
| 10 |
|
66
|
+
| 100 |
|
67
|
+
| 1000 |
|
@@ -0,0 +1,34 @@
|
|
1
|
+
When /^the application issues an? "([A-Z]+)" request for "([^"]*)"$/ do |method, url|
|
2
|
+
case method
|
3
|
+
when "GET"
|
4
|
+
get :from => url
|
5
|
+
when "POST"
|
6
|
+
post :to => url
|
7
|
+
when "DELETE"
|
8
|
+
delete :at => url
|
9
|
+
when "PUT"
|
10
|
+
put :to => url
|
11
|
+
else
|
12
|
+
method.should == "unsupported HTTP method"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
When /^the application issues an? "([A-Z]+)" request to the mock$/ do |method|
|
17
|
+
When %{the application issues a "#{method}" request for "/service"}
|
18
|
+
end
|
19
|
+
|
20
|
+
Then /^the response code should be "(\d+)"$/ do |expected|
|
21
|
+
response.code.should == expected.to_i
|
22
|
+
end
|
23
|
+
|
24
|
+
Then /^the response "([^"]*)" header should be "([^"]*)"$/ do |header, expected|
|
25
|
+
response.headers[header].should match(expected)
|
26
|
+
end
|
27
|
+
|
28
|
+
Then /^the response body should be:$/ do |expected|
|
29
|
+
response.body.should == expected
|
30
|
+
end
|
31
|
+
|
32
|
+
Then /^the response time should be at least "(\d*)"$/ do |min_time|
|
33
|
+
response.time.should >= min_time.to_i
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
Given /^the following response mock, known as "([^"]*)":$/ do |name, mock|
|
2
|
+
known_mock name, :body => mock
|
3
|
+
end
|
4
|
+
|
5
|
+
Given /^a defined response mock with a "([^"]*)" of "([^"]*)"$/ do |field, value|
|
6
|
+
known_mock "defined", :body => { :url => "/service",
|
7
|
+
:method => "GET",
|
8
|
+
'content-type' => "text/plain",
|
9
|
+
:body => "fake body" }.merge(field.to_sym => value).to_json
|
10
|
+
define_mock "defined"
|
11
|
+
end
|
12
|
+
|
13
|
+
When %r{^the application POSTs the mock "([^"]*)" to "/h/mocks"$} do |name|
|
14
|
+
define_mock name
|
15
|
+
end
|
16
|
+
|
17
|
+
When /^the application removes the mock "([^"]*)"$/ do |name|
|
18
|
+
remove_mock name
|
19
|
+
end
|
20
|
+
|
21
|
+
Then /^the "([^"]*)" mock is available at the URL in the "([^"]*)" header$/ do |name, header|
|
22
|
+
id_url = response.headers[header]
|
23
|
+
known_mock name, :url => id_url
|
24
|
+
get :from => id_url
|
25
|
+
response.code.should == 200
|
26
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# Taken from the cucumber-rails project.
|
2
|
+
# IMPORTANT: This file is generated by cucumber-sinatra - edit at your own peril.
|
3
|
+
# It is recommended to regenerate this file in the future when you upgrade to a
|
4
|
+
# newer version of cucumber-sinatra. Consider adding your own code to a new file
|
5
|
+
# instead of editing this one. Cucumber will automatically load all features/**/*.rb
|
6
|
+
# files.
|
7
|
+
|
8
|
+
require 'uri'
|
9
|
+
require 'cgi'
|
10
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths"))
|
11
|
+
|
12
|
+
module WithinHelpers
|
13
|
+
def with_scope(locator)
|
14
|
+
locator ? within(locator) { yield } : yield
|
15
|
+
end
|
16
|
+
end
|
17
|
+
World(WithinHelpers)
|
18
|
+
|
19
|
+
Given /^(?:|I )am on (.+)$/ do |page_name|
|
20
|
+
visit path_to(page_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
When /^(?:|I )go to (.+)$/ do |page_name|
|
24
|
+
visit path_to(page_name)
|
25
|
+
end
|
26
|
+
|
27
|
+
When /^(?:|I )press "([^\"]*)"(?: within "([^\"]*)")?$/ do |button, selector|
|
28
|
+
with_scope(selector) do
|
29
|
+
click_button(button)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
When /^(?:|I )follow "([^\"]*)"(?: within "([^\"]*)")?$/ do |link, selector|
|
34
|
+
with_scope(selector) do
|
35
|
+
click_link(link)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
When /^(?:|I )fill in "([^\"]*)" with "([^\"]*)"(?: within "([^\"]*)")?$/ do |field, value, selector|
|
40
|
+
with_scope(selector) do
|
41
|
+
fill_in(field, :with => value)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
When /^(?:|I )fill in "([^\"]*)" for "([^\"]*)"(?: within "([^\"]*)")?$/ do |value, field, selector|
|
46
|
+
with_scope(selector) do
|
47
|
+
fill_in(field, :with => value)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Use this to fill in an entire form with data from a table. Example:
|
52
|
+
#
|
53
|
+
# When I fill in the following:
|
54
|
+
# | Account Number | 5002 |
|
55
|
+
# | Expiry date | 2009-11-01 |
|
56
|
+
# | Note | Nice guy |
|
57
|
+
# | Wants Email? | |
|
58
|
+
#
|
59
|
+
# TODO: Add support for checkbox, select og option
|
60
|
+
# based on naming conventions.
|
61
|
+
#
|
62
|
+
When /^(?:|I )fill in the following(?: within "([^\"]*)")?:$/ do |selector, fields|
|
63
|
+
with_scope(selector) do
|
64
|
+
fields.rows_hash.each do |name, value|
|
65
|
+
When %{I fill in "#{name}" with "#{value}"}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
When /^(?:|I )select "([^\"]*)" from "([^\"]*)"(?: within "([^\"]*)")?$/ do |value, field, selector|
|
71
|
+
with_scope(selector) do
|
72
|
+
select(value, :from => field)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
When /^(?:|I )check "([^\"]*)"(?: within "([^\"]*)")?$/ do |field, selector|
|
77
|
+
with_scope(selector) do
|
78
|
+
check(field)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
When /^(?:|I )uncheck "([^\"]*)"(?: within "([^\"]*)")?$/ do |field, selector|
|
83
|
+
with_scope(selector) do
|
84
|
+
uncheck(field)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
When /^(?:|I )choose "([^\"]*)"(?: within "([^\"]*)")?$/ do |field, selector|
|
89
|
+
with_scope(selector) do
|
90
|
+
choose(field)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
When /^(?:|I )attach the file "([^\"]*)" to "([^\"]*)"(?: within "([^\"]*)")?$/ do |path, field, selector|
|
95
|
+
with_scope(selector) do
|
96
|
+
attach_file(field, path)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
Then /^(?:|I )should see JSON:$/ do |expected_json|
|
101
|
+
require 'json'
|
102
|
+
expected = JSON.pretty_generate(JSON.parse(expected_json))
|
103
|
+
actual = JSON.pretty_generate(JSON.parse(response.body))
|
104
|
+
expected.should == actual
|
105
|
+
end
|
106
|
+
|
107
|
+
Then /^(?:|I )should see "([^\"]*)"(?: within "([^\"]*)")?$/ do |text, selector|
|
108
|
+
with_scope(selector) do
|
109
|
+
if page.respond_to? :should
|
110
|
+
page.should have_content(text)
|
111
|
+
else
|
112
|
+
assert page.has_content?(text)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
Then /^(?:|I )should see \/([^\/]*)\/(?: within "([^\"]*)")?$/ do |regexp, selector|
|
118
|
+
regexp = Regexp.new(regexp)
|
119
|
+
with_scope(selector) do
|
120
|
+
if page.respond_to? :should
|
121
|
+
page.should have_xpath('//*', :text => regexp)
|
122
|
+
else
|
123
|
+
assert page.has_xpath?('//*', :text => regexp)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
Then /^(?:|I )should not see "([^\"]*)"(?: within "([^\"]*)")?$/ do |text, selector|
|
129
|
+
with_scope(selector) do
|
130
|
+
if page.respond_to? :should
|
131
|
+
page.should have_no_content(text)
|
132
|
+
else
|
133
|
+
assert page.has_no_content?(text)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
Then /^(?:|I )should not see \/([^\/]*)\/(?: within "([^\"]*)")?$/ do |regexp, selector|
|
139
|
+
regexp = Regexp.new(regexp)
|
140
|
+
with_scope(selector) do
|
141
|
+
if page.respond_to? :should
|
142
|
+
page.should have_no_xpath('//*', :text => regexp)
|
143
|
+
else
|
144
|
+
assert page.has_no_xpath?('//*', :text => regexp)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
Then /^the "([^\"]*)" field(?: within "([^\"]*)")? should contain "([^\"]*)"$/ do |field, selector, value|
|
150
|
+
with_scope(selector) do
|
151
|
+
field = find_field(field)
|
152
|
+
field_value = (field.tag_name == 'textarea') ? field.text : field.value
|
153
|
+
if field_value.respond_to? :should
|
154
|
+
field_value.should =~ /#{value}/
|
155
|
+
else
|
156
|
+
assert_match(/#{value}/, field_value)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
Then /^the "([^\"]*)" field(?: within "([^\"]*)")? should not contain "([^\"]*)"$/ do |field, selector, value|
|
162
|
+
with_scope(selector) do
|
163
|
+
field = find_field(field)
|
164
|
+
field_value = (field.tag_name == 'textarea') ? field.text : field.value
|
165
|
+
if field_value.respond_to? :should_not
|
166
|
+
field_value.should_not =~ /#{value}/
|
167
|
+
else
|
168
|
+
assert_no_match(/#{value}/, field_value)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
Then /^the "([^\"]*)" checkbox(?: within "([^\"]*)")? should be checked$/ do |label, selector|
|
174
|
+
with_scope(selector) do
|
175
|
+
field_checked = find_field(label)['checked']
|
176
|
+
if field_checked.respond_to? :should
|
177
|
+
field_checked.should == 'checked'
|
178
|
+
else
|
179
|
+
assert_equal 'checked', field_checked
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
Then /^the "([^\"]*)" checkbox(?: within "([^\"]*)")? should not be checked$/ do |label, selector|
|
185
|
+
with_scope(selector) do
|
186
|
+
field_checked = find_field(label)['checked']
|
187
|
+
if field_checked.respond_to? :should_not
|
188
|
+
field_checked.should_not == 'checked'
|
189
|
+
else
|
190
|
+
assert_not_equal 'checked', field_checked
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
Then /^(?:|I )should be on (.+)$/ do |page_name|
|
196
|
+
current_path = URI.parse(current_url).path
|
197
|
+
if current_path.respond_to? :should
|
198
|
+
current_path.should == path_to(page_name)
|
199
|
+
else
|
200
|
+
assert_equal path_to(page_name), current_path
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
Then /^(?:|I )should have the following query string:$/ do |expected_pairs|
|
205
|
+
query = URI.parse(current_url).query
|
206
|
+
actual_params = query ? CGI.parse(query) : {}
|
207
|
+
expected_params = {}
|
208
|
+
expected_pairs.rows_hash.each_pair{|k,v| expected_params[k] = v.split(',')}
|
209
|
+
|
210
|
+
if actual_params.respond_to? :should
|
211
|
+
actual_params.should == expected_params
|
212
|
+
else
|
213
|
+
assert_equal expected_params, actual_params
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
Then /^show me the page$/ do
|
218
|
+
save_and_open_page
|
219
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'json'
|
3
|
+
require 'benchmark'
|
4
|
+
|
5
|
+
module HTTParty
|
6
|
+
class Response
|
7
|
+
attr_accessor :time
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class ClientApplication
|
12
|
+
include HTTParty
|
13
|
+
|
14
|
+
base_uri 'example.com'
|
15
|
+
|
16
|
+
attr_reader :response
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@known_mocks = Hash.new { |h, k| h[k] = {} }
|
20
|
+
end
|
21
|
+
|
22
|
+
def known_mock(name, description = {})
|
23
|
+
@known_mocks[name].merge!(description)
|
24
|
+
end
|
25
|
+
|
26
|
+
def define_mock(name)
|
27
|
+
@response = self.class.post "/h/mocks", :body => @known_mocks[name][:body]
|
28
|
+
end
|
29
|
+
|
30
|
+
def remove_mock(name)
|
31
|
+
@response = self.class.delete @known_mocks[name][:url]
|
32
|
+
end
|
33
|
+
|
34
|
+
def get(options)
|
35
|
+
timed { @response = self.class.get options[:from] }
|
36
|
+
end
|
37
|
+
|
38
|
+
def post(options)
|
39
|
+
timed { @response = self.class.post options[:to] }
|
40
|
+
end
|
41
|
+
|
42
|
+
def delete(options)
|
43
|
+
timed { @response = self.class.delete options[:at] }
|
44
|
+
end
|
45
|
+
|
46
|
+
def put(options)
|
47
|
+
timed { @response = self.class.put options[:to] }
|
48
|
+
end
|
49
|
+
|
50
|
+
def timed
|
51
|
+
val = nil
|
52
|
+
timed = time { val = yield }
|
53
|
+
val.time = timed
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Generated by cucumber-sinatra. (Wed May 25 23:29:21 -0700 2011)
|
2
|
+
|
3
|
+
ENV['RACK_ENV'] = 'test'
|
4
|
+
|
5
|
+
require File.join(File.dirname(__FILE__), '..', '..', 'lib/harper.rb')
|
6
|
+
require File.join(File.dirname(__FILE__), 'client_application')
|
7
|
+
|
8
|
+
require 'sham_rack'
|
9
|
+
require 'rspec'
|
10
|
+
|
11
|
+
require File.join(File.dirname(__FILE__), '..', '..', 'spec/spec_helper')
|
12
|
+
|
13
|
+
class HarperWorld
|
14
|
+
include RSpec::Expectations
|
15
|
+
include RSpec::Matchers
|
16
|
+
end
|
17
|
+
|
18
|
+
World do
|
19
|
+
HarperWorld.new
|
20
|
+
ClientApplication.new
|
21
|
+
end
|
22
|
+
|
23
|
+
ShamRack.at("example.com").rackup do
|
24
|
+
run Harper.new
|
25
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Taken from the cucumber-rails project.
|
2
|
+
|
3
|
+
module NavigationHelpers
|
4
|
+
# Maps a name to a path. Used by the
|
5
|
+
#
|
6
|
+
# When /^I go to (.+)$/ do |page_name|
|
7
|
+
#
|
8
|
+
# step definition in web_steps.rb
|
9
|
+
#
|
10
|
+
def path_to(page_name)
|
11
|
+
case page_name
|
12
|
+
|
13
|
+
when /the home\s?page/
|
14
|
+
'/'
|
15
|
+
|
16
|
+
# Add more mappings here.
|
17
|
+
# Here is an example that pulls values out of the Regexp:
|
18
|
+
#
|
19
|
+
# when /^(.*)'s profile page$/i
|
20
|
+
# user_profile_path(User.find_by_login($1))
|
21
|
+
|
22
|
+
else
|
23
|
+
raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
|
24
|
+
"Now, go and add a mapping in #{__FILE__}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
World(NavigationHelpers)
|
data/harper.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "harper"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "harper"
|
7
|
+
s.version = Harper::Version
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Giles Alexander"]
|
10
|
+
s.email = ["giles.alexander@gmail.com"]
|
11
|
+
s.homepage = "http://github.com/gga/harper"
|
12
|
+
s.summary = %q{Dead simple mocking for HTTP services}
|
13
|
+
s.description = %q{Dead simple mocking for HTTP services, a Sinatra app}
|
14
|
+
|
15
|
+
s.rubyforge_project = "harper"
|
16
|
+
|
17
|
+
s.required_rubygems_version = ">= 1.3.6"
|
18
|
+
|
19
|
+
s.add_dependency "sinatra", ">= 1.0.0"
|
20
|
+
s.add_dependency "httparty"
|
21
|
+
s.add_dependency "json", ">= 1.4.6"
|
22
|
+
|
23
|
+
s.add_development_dependency "rack-test"
|
24
|
+
s.add_development_dependency "sham_rack"
|
25
|
+
s.add_development_dependency "rspec"
|
26
|
+
s.add_development_dependency "cucumber", ">= 0.10"
|
27
|
+
s.add_development_dependency "cucumber-sinatra"
|
28
|
+
|
29
|
+
s.files = `git ls-files`.split("\n")
|
30
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
31
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
32
|
+
s.require_paths = ["lib"]
|
33
|
+
end
|
data/lib/harper.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'logger'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
class Harper < Sinatra::Base
|
7
|
+
|
8
|
+
Version = "0.0.2"
|
9
|
+
|
10
|
+
@@mocks = {}
|
11
|
+
|
12
|
+
enable :logging
|
13
|
+
|
14
|
+
configure do
|
15
|
+
LOGGER = Logger.new("sinatra.log")
|
16
|
+
end
|
17
|
+
|
18
|
+
helpers do
|
19
|
+
def logger
|
20
|
+
LOGGER
|
21
|
+
end
|
22
|
+
|
23
|
+
def mock_id(url)
|
24
|
+
[url].pack('m').tr("+/=", "-_.").gsub("\n", '')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
post '/h/mocks' do
|
29
|
+
mock = JSON(request.body.read)
|
30
|
+
|
31
|
+
mock['url'] = mock['url'][1..-1] if mock['url'] =~ /^\//
|
32
|
+
|
33
|
+
mock['id'] = mock_id(mock['url'])
|
34
|
+
mock['method'].upcase!
|
35
|
+
mock['delay'] = mock['delay'].to_f / 1000.0
|
36
|
+
@@mocks[mock['id']] = mock
|
37
|
+
|
38
|
+
logger.info("Created mock for endpoint: '#{mock['url']}'")
|
39
|
+
|
40
|
+
headers['location'] = "/h/mocks/#{mock['id']}"
|
41
|
+
status "201"
|
42
|
+
end
|
43
|
+
|
44
|
+
delete '/h/mocks' do
|
45
|
+
@@mocks = {}
|
46
|
+
end
|
47
|
+
|
48
|
+
get '/h/mocks/:mock_id' do |mock_name|
|
49
|
+
content_type :json
|
50
|
+
status "200"
|
51
|
+
@@mocks[mock_name].to_json
|
52
|
+
end
|
53
|
+
|
54
|
+
delete '/h/mocks/:mock_id' do |mock_name|
|
55
|
+
@@mocks[mock_name] = nil
|
56
|
+
|
57
|
+
status "200"
|
58
|
+
end
|
59
|
+
|
60
|
+
[:get, :post, :put, :delete].each do |method|
|
61
|
+
self.send(method, '*') do
|
62
|
+
mock_id = mock_id(request.path[1..-1])
|
63
|
+
|
64
|
+
logger.debug("#{request.request_method} request for a mock: '#{request.path}'")
|
65
|
+
|
66
|
+
mock = @@mocks[mock_id]
|
67
|
+
if mock && request.request_method == mock['method']
|
68
|
+
content_type mock['content-type']
|
69
|
+
status mock['status'] || "200"
|
70
|
+
sleep mock['delay']
|
71
|
+
|
72
|
+
logger.info("Serving mocked body for endpoint: '#{mock['url']}'")
|
73
|
+
|
74
|
+
mock['body']
|
75
|
+
else
|
76
|
+
status "503"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
data/spec/harper_spec.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
require 'harper'
|
3
|
+
|
4
|
+
describe Harper do
|
5
|
+
include Rack::Test::Methods
|
6
|
+
|
7
|
+
supported_verbs = ["GET", "POST", "PUT", "DELETE"]
|
8
|
+
|
9
|
+
let(:app) { Harper.new }
|
10
|
+
|
11
|
+
let(:method) { "GET" }
|
12
|
+
let(:url) { "/service" }
|
13
|
+
let(:status_code) { 200 }
|
14
|
+
let(:content_type) { "text/plain" }
|
15
|
+
let(:body) { "fake body" }
|
16
|
+
let(:delay) { 0 }
|
17
|
+
|
18
|
+
let(:mock_def) do
|
19
|
+
{ :method => method,
|
20
|
+
:url => url,
|
21
|
+
:delay => delay,
|
22
|
+
:'content-type' => content_type,
|
23
|
+
:body => body }.to_json
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should allow all mocks to be deleted" do
|
27
|
+
post '/h/mocks', mock_def
|
28
|
+
|
29
|
+
delete '/h/mocks'
|
30
|
+
last_response.should be_ok
|
31
|
+
get url
|
32
|
+
last_response.status.should == 503
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "mock methods are case insensitive:" do
|
36
|
+
supported_verbs.each do |http_verb|
|
37
|
+
[lambda { |v| v.upcase },
|
38
|
+
lambda { |v| v.downcase },
|
39
|
+
lambda { |v| v[0..0] + v[1..-1].downcase}].each do |fn|
|
40
|
+
cased_verb = fn.call(http_verb)
|
41
|
+
context cased_verb do
|
42
|
+
let(:method) { cased_verb }
|
43
|
+
|
44
|
+
before(:each) do
|
45
|
+
post '/h/mocks', mock_def
|
46
|
+
@created_mock = last_response.headers['location']
|
47
|
+
end
|
48
|
+
|
49
|
+
after(:each) do
|
50
|
+
delete @created_mock
|
51
|
+
end
|
52
|
+
|
53
|
+
it "is a valid method" do
|
54
|
+
self.send(cased_verb.downcase.to_sym, url)
|
55
|
+
last_response.should be_ok
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
supported_verbs.each do |http_verb|
|
63
|
+
context "#{http_verb} mock" do
|
64
|
+
let(:method) { http_verb }
|
65
|
+
|
66
|
+
before(:each) do
|
67
|
+
post '/h/mocks', mock_def
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should respond with a 201 created" do
|
71
|
+
last_response.status.should == 201
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should point to a newly created mock resource" do
|
75
|
+
last_response.headers['Location'].should match(%r{/h/mocks/})
|
76
|
+
end
|
77
|
+
|
78
|
+
context "long service urls" do
|
79
|
+
let(:url) { "http://www.averylong.com/url/thathas/multiple/slashes/and/thelike/" }
|
80
|
+
it "should be queryable" do
|
81
|
+
get last_response.headers['Location']
|
82
|
+
last_response.status.should == 200
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "is a mock" do
|
87
|
+
before(:each) do
|
88
|
+
self.send(method.downcase.to_sym, url)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "has a mocked status code" do
|
92
|
+
last_response.status.should == status_code
|
93
|
+
end
|
94
|
+
it "has a mocked content-type" do
|
95
|
+
last_response.headers['Content-Type'].should match(content_type)
|
96
|
+
end
|
97
|
+
it "has a mocked body" do
|
98
|
+
last_response.body.should == body
|
99
|
+
end
|
100
|
+
it "should not support unmocked HTTP methods" do
|
101
|
+
(supported_verbs - [method]).each do |non_method|
|
102
|
+
self.send(non_method.downcase.to_sym, url)
|
103
|
+
last_response.status.should == 503
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should allow the mock resource to be deleted" do
|
109
|
+
delete last_response.headers['Location']
|
110
|
+
last_response.should be_ok
|
111
|
+
self.send(method.downcase.to_sym, url)
|
112
|
+
last_response.status.should == 503
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context "delayed mocks" do
|
118
|
+
|
119
|
+
before(:each) do
|
120
|
+
post '/h/mocks', mock_def
|
121
|
+
@created_mock = last_response.headers['location']
|
122
|
+
end
|
123
|
+
|
124
|
+
after(:each) do
|
125
|
+
delete @created_mock
|
126
|
+
end
|
127
|
+
|
128
|
+
context "short delay" do
|
129
|
+
let(:delay) { 100 }
|
130
|
+
|
131
|
+
it "should take at least the specified delay to provide a response" do
|
132
|
+
time { get url }.should >= delay
|
133
|
+
end
|
134
|
+
end
|
135
|
+
context "long delay" do
|
136
|
+
let(:delay) { 1000 }
|
137
|
+
|
138
|
+
it "should take at least the specified delay to provide a response" do
|
139
|
+
time { get url }.should >= delay
|
140
|
+
end
|
141
|
+
end
|
142
|
+
context "no delay specified" do
|
143
|
+
let(:delay) { nil }
|
144
|
+
|
145
|
+
it "should take close to 0 delay in providing a response" do
|
146
|
+
time { get url }.should <= 1.0
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: harper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Giles Alexander
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-06-15 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: sinatra
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 23
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 0
|
33
|
+
- 0
|
34
|
+
version: 1.0.0
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: httparty
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 3
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
version: "0"
|
49
|
+
type: :runtime
|
50
|
+
version_requirements: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: json
|
53
|
+
prerelease: false
|
54
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 11
|
60
|
+
segments:
|
61
|
+
- 1
|
62
|
+
- 4
|
63
|
+
- 6
|
64
|
+
version: 1.4.6
|
65
|
+
type: :runtime
|
66
|
+
version_requirements: *id003
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: rack-test
|
69
|
+
prerelease: false
|
70
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
hash: 3
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
type: :development
|
80
|
+
version_requirements: *id004
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: sham_rack
|
83
|
+
prerelease: false
|
84
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
hash: 3
|
90
|
+
segments:
|
91
|
+
- 0
|
92
|
+
version: "0"
|
93
|
+
type: :development
|
94
|
+
version_requirements: *id005
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: rspec
|
97
|
+
prerelease: false
|
98
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
hash: 3
|
104
|
+
segments:
|
105
|
+
- 0
|
106
|
+
version: "0"
|
107
|
+
type: :development
|
108
|
+
version_requirements: *id006
|
109
|
+
- !ruby/object:Gem::Dependency
|
110
|
+
name: cucumber
|
111
|
+
prerelease: false
|
112
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
hash: 31
|
118
|
+
segments:
|
119
|
+
- 0
|
120
|
+
- 10
|
121
|
+
version: "0.10"
|
122
|
+
type: :development
|
123
|
+
version_requirements: *id007
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: cucumber-sinatra
|
126
|
+
prerelease: false
|
127
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
128
|
+
none: false
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
hash: 3
|
133
|
+
segments:
|
134
|
+
- 0
|
135
|
+
version: "0"
|
136
|
+
type: :development
|
137
|
+
version_requirements: *id008
|
138
|
+
description: Dead simple mocking for HTTP services, a Sinatra app
|
139
|
+
email:
|
140
|
+
- giles.alexander@gmail.com
|
141
|
+
executables:
|
142
|
+
- harper
|
143
|
+
extensions: []
|
144
|
+
|
145
|
+
extra_rdoc_files: []
|
146
|
+
|
147
|
+
files:
|
148
|
+
- .gitignore
|
149
|
+
- .rvmrc
|
150
|
+
- Gemfile
|
151
|
+
- Gemfile.lock
|
152
|
+
- Rakefile
|
153
|
+
- bin/harper
|
154
|
+
- features/mock_http_rpc_service.feature
|
155
|
+
- features/step_definitions/http_steps.rb
|
156
|
+
- features/step_definitions/mock_definition_steps.rb
|
157
|
+
- features/step_definitions/web_steps.rb
|
158
|
+
- features/support/client_application.rb
|
159
|
+
- features/support/env.rb
|
160
|
+
- features/support/paths.rb
|
161
|
+
- harper.gemspec
|
162
|
+
- lib/harper.rb
|
163
|
+
- spec/harper_spec.rb
|
164
|
+
- spec/spec_helper.rb
|
165
|
+
has_rdoc: true
|
166
|
+
homepage: http://github.com/gga/harper
|
167
|
+
licenses: []
|
168
|
+
|
169
|
+
post_install_message:
|
170
|
+
rdoc_options: []
|
171
|
+
|
172
|
+
require_paths:
|
173
|
+
- lib
|
174
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
175
|
+
none: false
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
hash: 3
|
180
|
+
segments:
|
181
|
+
- 0
|
182
|
+
version: "0"
|
183
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
184
|
+
none: false
|
185
|
+
requirements:
|
186
|
+
- - ">="
|
187
|
+
- !ruby/object:Gem::Version
|
188
|
+
hash: 23
|
189
|
+
segments:
|
190
|
+
- 1
|
191
|
+
- 3
|
192
|
+
- 6
|
193
|
+
version: 1.3.6
|
194
|
+
requirements: []
|
195
|
+
|
196
|
+
rubyforge_project: harper
|
197
|
+
rubygems_version: 1.6.2
|
198
|
+
signing_key:
|
199
|
+
specification_version: 3
|
200
|
+
summary: Dead simple mocking for HTTP services
|
201
|
+
test_files:
|
202
|
+
- features/mock_http_rpc_service.feature
|
203
|
+
- features/step_definitions/http_steps.rb
|
204
|
+
- features/step_definitions/mock_definition_steps.rb
|
205
|
+
- features/step_definitions/web_steps.rb
|
206
|
+
- features/support/client_application.rb
|
207
|
+
- features/support/env.rb
|
208
|
+
- features/support/paths.rb
|
209
|
+
- spec/harper_spec.rb
|
210
|
+
- spec/spec_helper.rb
|