harper 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.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
|