rspec-crampy 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +126 -0
- data/Rakefile +12 -0
- data/lib/rspec/cramp.rb +7 -0
- data/lib/rspec/cramp/extensions/cramp/action.rb +17 -0
- data/lib/rspec/cramp/matchers/respond_with.rb +18 -0
- data/lib/rspec/cramp/mock_response.rb +151 -0
- data/lib/rspec/cramp/shared_context.rb +126 -0
- data/rspec-cramp.gemspec +22 -0
- data/spec/examples/basic_spec.rb +60 -0
- data/spec/examples/errors_spec.rb +26 -0
- data/spec/examples/headers_spec.rb +25 -0
- data/spec/examples/low_level_spec.rb +26 -0
- data/spec/examples/sample_actions.rb +42 -0
- data/spec/rspec_cramp_spec.rb +513 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/tests.log +1417 -0
- metadata +129 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: aaef9b457ed6d80175369fb71452b0d1b2e23db0
|
4
|
+
data.tar.gz: 58f196d3e716d17a08d796f52e503cb6171d847b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0268f3da2001efd1a433ef60819a6125247e2110818ead27594248a4e8c074e39985e80e2c00f280c8669b95490a3c8c10dc4000c7ea72e5dc769e10a9aee3aa
|
7
|
+
data.tar.gz: 8f14f10472bd3a9de281a6eaff9fe9b338671f4a2fe4d9013c6c6ee5eb43face0b9f30e7fc967ea483d3aee474cb53877388fad22ccf7b9f64b9bf3d671ea9ad
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Martin Bilski
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
A set of rspec matchers and helpers that make it easier to write specs for [cramp](http://cramp.in).
|
2
|
+
|
3
|
+
Quick start
|
4
|
+
-----------
|
5
|
+
|
6
|
+
> gem install rspec-cramp
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
require 'rspec/cramp'
|
11
|
+
|
12
|
+
# Decorate your spec with :cramp => true
|
13
|
+
describe HelloWorld, :cramp => true do
|
14
|
+
|
15
|
+
# You need to define this method
|
16
|
+
def app
|
17
|
+
HelloWorld # Here goes your cramp application, action or http routes.
|
18
|
+
end
|
19
|
+
|
20
|
+
# Matching on status code.
|
21
|
+
it "should respond to a GET request" do
|
22
|
+
get("/").should respond_with :status => :ok # Matches responses from 200 to 299.
|
23
|
+
get("/").should respond_with :status => 200 # Matches only 200.
|
24
|
+
get("/").should respond_with :status => "200" # Same as above.
|
25
|
+
get("/").should respond_with :status => /^2.*/ # Matches response codes starting with 2.
|
26
|
+
get("/").should_not respond_with :status => :error # Matches any HTTP error.
|
27
|
+
end
|
28
|
+
|
29
|
+
# Matching on response body.
|
30
|
+
it "should respond with text starting with 'Hello'" do
|
31
|
+
get("/").should respond_with :body => /^Hello.*/
|
32
|
+
end
|
33
|
+
it "should respond with 'Hello, world!'" do
|
34
|
+
get("/").should respond_with :body => "Hello, world!"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Matching on response headers.
|
38
|
+
it "should respond with html" do
|
39
|
+
get("/").should respond_with :headers => {"Content-Type" => "text/html"}
|
40
|
+
get("/").should_not respond_with :headers => {"Content-Type" => "text/plain"}
|
41
|
+
get("/").should_not respond_with :headers => {"Unexpected-Header" => /.*/}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Matching using lambdas.
|
45
|
+
it "should match my sophisticated custom matchers" do
|
46
|
+
# Entire headers.
|
47
|
+
status_check = lambda {|status| status.between?(200, 299)}
|
48
|
+
body_check = lambda {|body| body =~ /.*el.*/}
|
49
|
+
headers_check = lambda {|headers| true} # Any headers will do.
|
50
|
+
get("/").should respond_with :status => status_check, :body => body_check, :headers => headers_check
|
51
|
+
# Header value.
|
52
|
+
get("/").should respond_with :headers => {"Content-Type" => lambda {|value| value == "text/html"}}
|
53
|
+
get("/").should_not respond_with :headers => {"Content-Type" => lambda {|value| value == "text/plain"}}
|
54
|
+
end
|
55
|
+
|
56
|
+
# Supports POST/GET/PUT/DELETE and you don't have to use the matcher.
|
57
|
+
it "should work without a matcher" do
|
58
|
+
get "/"
|
59
|
+
post "/"
|
60
|
+
put "/"
|
61
|
+
delete "/"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Request params & custom headers.
|
65
|
+
it "should accept my params" do
|
66
|
+
post("/", :params => {:text => "whatever"}, :headers => {"Custom-Header" => "blah"})
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
The matcher is fairly flexible, supports regular expressions and also works with multipart responses (more than one `Cramp::Action.render`), SSE and so on. I'll create more examples and docs but for the time being, pls. take a look at the code and [examples](https://github.com/bilus/rspec-cramp/tree/master/spec/examples).
|
71
|
+
|
72
|
+
**DISCLAIMER:** I haven't done any work with WebSockets yet so if there is anyone willing to add support for WebSockets, please [tweet me](http://twitter.com/#!/MartinBilski)
|
73
|
+
|
74
|
+
Project status
|
75
|
+
--------------
|
76
|
+
|
77
|
+
**IMPORTANT:** This is work in progress.
|
78
|
+
|
79
|
+
There are still some things I'll take care of soon (esp. better failure messages).
|
80
|
+
|
81
|
+
If you have any comments regarding the code as it is now (I know it's a bit messy), please feel free to tweet [@MartinBilski](http://twitter.com/#!/MartinBilski)
|
82
|
+
|
83
|
+
Contributors
|
84
|
+
------------
|
85
|
+
|
86
|
+
[Ivan Fomichev](https://github.com/codeholic)
|
87
|
+
|
88
|
+
Notes
|
89
|
+
----
|
90
|
+
|
91
|
+
1. The previous version had a problem handling exceptions raised in `on_start` or `on_finish` or with `on_start` that called `finish` without rendering anything because the request helper methods always try to read one body chunk after a successful response (200-299).
|
92
|
+
|
93
|
+
Actually, it wasn't a very big deal, the call would simply time out and raise `Timeout::Error` with a special error message hinting at the problem: *execution expired (No render call in action?)*.
|
94
|
+
|
95
|
+
The current version, comes with a [monkey-patched `Cramp::Action`](https://github.com/bilus/rspec-cramp/tree/master/lib/rspec_cramp.rb) which now renders the exception info if it is raised in `on_start` or in `on_finish`.
|
96
|
+
|
97
|
+
See [this example spec](https://github.com/bilus/rspec-cramp/tree/master/spec/examples/errors_spec.rb) to see error handling in action.
|
98
|
+
|
99
|
+
I'm definitely open to suggestions, especially how this can be fixed without the cramp surgery. Is the original timeout-based solution better? Unfortunately, the matcher by default always loads one chunk of response body for a successful response.
|
100
|
+
|
101
|
+
2. This work is based on [Pratik Naik's code](https://github.com/lifo/cramp/blob/master/lib/cramp/test_case.rb) and writing specs in a similar fashion is still supported though I added a helper for loading the body and a response matcher and some accessors to make your life easier.
|
102
|
+
|
103
|
+
describe MyCrampAction, :cramp => true do
|
104
|
+
def app
|
105
|
+
MyCrampAction
|
106
|
+
end
|
107
|
+
it "should respond to a GET request" do
|
108
|
+
get("/") do |response|
|
109
|
+
response.status.should == 200
|
110
|
+
response.headers.should have_key "Content-Type"
|
111
|
+
response.should be_matching :status => :ok
|
112
|
+
stop # This is important.
|
113
|
+
end
|
114
|
+
end
|
115
|
+
it "should match the body" do
|
116
|
+
get("/") do |response|
|
117
|
+
response.read_body do
|
118
|
+
response.body.should include "Hello, world!" # MockResponse::body returns an Array.
|
119
|
+
response.should be_matching :body => "Hello, world!"
|
120
|
+
# Note: no call to stop here.
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
In general, I recommend using the 'respond_with' matcher whenever possible; I think it makes the specs more readable because it hides some gory details (for good or bad). But they are useful when you're debugging your cramp application if the failure message doesn't include all the details you need.
|
data/Rakefile
ADDED
data/lib/rspec/cramp.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Monkey-patch so that even if there is an exception raised in on_start or in on_finish
|
2
|
+
# the exception dump is rendered so that:
|
3
|
+
# - you can match against it in your spec,
|
4
|
+
# - get method doesn't time out waiting for anything to be rendered (may not be true for on_finish).
|
5
|
+
#
|
6
|
+
module Cramp
|
7
|
+
class Action
|
8
|
+
alias :old_handle_exception :handle_exception
|
9
|
+
def handle_exception(exception)
|
10
|
+
if @_state != :init
|
11
|
+
handler = ExceptionHandler.new(@env, exception)
|
12
|
+
render handler.pretty
|
13
|
+
end
|
14
|
+
old_handle_exception(exception)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# respond_to RSpec matcher.
|
2
|
+
# See spec/examples for sample usage.
|
3
|
+
#
|
4
|
+
RSpec::Matchers.define :respond_with do |options = {}|
|
5
|
+
match do |response|
|
6
|
+
@actual_response = response
|
7
|
+
response.matching?(options)
|
8
|
+
end
|
9
|
+
|
10
|
+
# TODO Better message for cases such as: specify { get("/").should respond_with :status => :ok }.
|
11
|
+
|
12
|
+
failure_message_for_should do
|
13
|
+
@actual_response.last_failure_message_for_should
|
14
|
+
end
|
15
|
+
failure_message_for_should_not do
|
16
|
+
@actual_response.last_failure_message_for_should_not
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Cramp
|
3
|
+
|
4
|
+
# Response from get, post etc. methods called in rspecs.
|
5
|
+
class MockResponse
|
6
|
+
def initialize(response)
|
7
|
+
@status = response[0]
|
8
|
+
@headers = response[1]
|
9
|
+
@body = response[2]
|
10
|
+
end
|
11
|
+
|
12
|
+
def read_body(max_chunks = 1, &block)
|
13
|
+
if @body.is_a? ::Cramp::Body
|
14
|
+
stopping = false
|
15
|
+
deferred_body = @body
|
16
|
+
chunks = []
|
17
|
+
check = lambda do
|
18
|
+
if chunks.count >= max_chunks
|
19
|
+
@body = chunks
|
20
|
+
stopping = true
|
21
|
+
block.call if block
|
22
|
+
EM.next_tick { EM.stop }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
check.call
|
26
|
+
deferred_body.each do |chunk|
|
27
|
+
chunks << chunk unless stopping
|
28
|
+
check.call
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def [](i)
|
34
|
+
[@status, @headers, @body][i]
|
35
|
+
end
|
36
|
+
|
37
|
+
def body
|
38
|
+
if @body.is_a? ::Cramp::Body
|
39
|
+
raise "Error: Something went wrong or body is not loaded yet (use response.read_body do { })."
|
40
|
+
end
|
41
|
+
@body
|
42
|
+
end
|
43
|
+
|
44
|
+
def headers
|
45
|
+
@headers
|
46
|
+
end
|
47
|
+
|
48
|
+
def status
|
49
|
+
@status
|
50
|
+
end
|
51
|
+
|
52
|
+
def matching?(match_options)
|
53
|
+
expected_status = match_options.delete(:status)
|
54
|
+
expected_header = match_options.delete(:headers)
|
55
|
+
expected_body = match_options.delete(:body)
|
56
|
+
expected_chunks = match_options.delete(:chunks)
|
57
|
+
raise "Unsupported match option" unless match_options.empty?
|
58
|
+
matching_status?(expected_status) && matching_headers?(expected_header) && matching_body?(expected_body) &&
|
59
|
+
matching_chunks?(expected_chunks)
|
60
|
+
end
|
61
|
+
|
62
|
+
def last_failure_message_for_should
|
63
|
+
# TODO Better failure message showing the specific mismatches that made it fail.
|
64
|
+
"expected #{@failure_info[:expected]} in #{@failure_info[:what].to_s} but got: #{@failure_info[:actual]}"
|
65
|
+
end
|
66
|
+
def last_failure_message_for_should_not
|
67
|
+
# TODO Better failure message showing the specific successful matches that made it fail.
|
68
|
+
"expected response not to match the conditions but got: #{[@status, @headers, @body].inspect}"
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def matching_response_element?(what, actual, expected)
|
74
|
+
is_match = if expected.nil?
|
75
|
+
true # No expectation set.
|
76
|
+
elsif actual.nil?
|
77
|
+
false
|
78
|
+
elsif expected.is_a? Regexp
|
79
|
+
actual.to_s.match(expected)
|
80
|
+
elsif expected.is_a? Integer
|
81
|
+
actual.to_i == expected
|
82
|
+
elsif expected.is_a? String
|
83
|
+
actual.to_s == expected
|
84
|
+
elsif expected.is_a? Proc
|
85
|
+
expected.call(actual)
|
86
|
+
else
|
87
|
+
raise "Unsupported type"
|
88
|
+
end
|
89
|
+
@failure_info = is_match ? {} : {:what => what, :actual => actual, :expected => format_expected(expected)}
|
90
|
+
is_match
|
91
|
+
end
|
92
|
+
|
93
|
+
def resolve_status(status)
|
94
|
+
case status
|
95
|
+
when :ok then /^2[0-9][0-9]$/
|
96
|
+
when :error then /^[^2][0-9][0-9]$/
|
97
|
+
else status
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def format_expected(expected)
|
102
|
+
expected.is_a?(Regexp) ? "/#{expected.source}/" : expected.inspect
|
103
|
+
end
|
104
|
+
|
105
|
+
def matching_status?(expected_status)
|
106
|
+
matching_response_element?(:status, @status, resolve_status(expected_status))
|
107
|
+
end
|
108
|
+
|
109
|
+
def matching_header_values?(expected_header)
|
110
|
+
expected_header.find do |ek, ev|
|
111
|
+
@headers.find { |ak, av| matching_response_element?(:headers, ak, ek) && !matching_response_element?(:headers, av, ev) } != nil
|
112
|
+
end == nil
|
113
|
+
end
|
114
|
+
|
115
|
+
def matching_header_keys?(expected_header)
|
116
|
+
is_match = @headers.keys.find do |actual|
|
117
|
+
expected_header.keys.find {|expected| matching_response_element?(:headers, actual, expected)} != nil
|
118
|
+
end != nil
|
119
|
+
@failure_info = is_match ? {} : {:what => :headers, :actual => @headers.keys.inspect, :expected => expected_header.keys.inspect}
|
120
|
+
is_match
|
121
|
+
end
|
122
|
+
|
123
|
+
def matching_headers?(expected_header)
|
124
|
+
if expected_header.nil?
|
125
|
+
true
|
126
|
+
elsif expected_header.is_a? Proc
|
127
|
+
matching_response_element?(:headers, @headers, expected_header)
|
128
|
+
else
|
129
|
+
matching_header_keys?(expected_header) && matching_header_values?(expected_header)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def matching_body?(expected_body)
|
134
|
+
actual_body = @body.is_a?(Array) ? @body.join("") : @body
|
135
|
+
matching_response_element?(:body, actual_body, expected_body)
|
136
|
+
end
|
137
|
+
|
138
|
+
def matching_chunks?(expected_chunks)
|
139
|
+
if expected_chunks.nil?
|
140
|
+
true
|
141
|
+
elsif expected_chunks.is_a? Proc
|
142
|
+
matching_response_element?(:chunks, @body, expected_chunks)
|
143
|
+
else
|
144
|
+
(@body.is_a?(Array) && @body.zip(expected_chunks).find do |actual, expected|
|
145
|
+
!matching_response_element?(:chunks, actual, expected)
|
146
|
+
end.nil?)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Cramp
|
3
|
+
# Usage:
|
4
|
+
#
|
5
|
+
# describe MyAction, :cramp => true do
|
6
|
+
# def app
|
7
|
+
# MyAction
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# it "should render home page" do
|
11
|
+
# get("/").should respond_with :status => :ok, :body => "Hello, world!"
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
shared_context "given a Cramp application", :cramp => true do
|
16
|
+
|
17
|
+
# In your describe block using :cramp => true, define a method called 'app' returning an async Rack application.
|
18
|
+
# Example:
|
19
|
+
#
|
20
|
+
# def app
|
21
|
+
# HelloWorldAction
|
22
|
+
# end
|
23
|
+
|
24
|
+
# Request helper method.
|
25
|
+
#
|
26
|
+
def request(method, path, options = {}, &block)
|
27
|
+
raise "Unsupported request method" unless [:get, :post, :delete, :put].include?(method)
|
28
|
+
if block
|
29
|
+
async_request(method, path, options, &block)
|
30
|
+
else
|
31
|
+
sync_request(method, path, options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# GET helper method.
|
36
|
+
#
|
37
|
+
def get(path, options = {}, &block)
|
38
|
+
request(:get, path, options, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
# POST helper method.
|
42
|
+
#
|
43
|
+
def post(path, options = {}, &block)
|
44
|
+
request(:post, path, options, &block)
|
45
|
+
end
|
46
|
+
|
47
|
+
# DELETE helper method.
|
48
|
+
#
|
49
|
+
def delete(path, options = {}, &block)
|
50
|
+
request(:delete, path, options, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
# PUT helper method.
|
54
|
+
#
|
55
|
+
def put(path, options = {}, &block)
|
56
|
+
request(:put, path, options, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Use it if using a block version of a request helper method.
|
60
|
+
# See spec/examples/low_level_spec.rb for examples.
|
61
|
+
#
|
62
|
+
def stop
|
63
|
+
EM.stop
|
64
|
+
end
|
65
|
+
|
66
|
+
# You can change the default timeout by overriding this method.
|
67
|
+
#
|
68
|
+
def default_timeout
|
69
|
+
3
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
before(:all) do
|
76
|
+
@request = Rack::MockRequest.new(app)
|
77
|
+
end
|
78
|
+
|
79
|
+
def async_request(method, path, options, &block)
|
80
|
+
callback = parse_response(block)
|
81
|
+
params = options.delete(:params)
|
82
|
+
headers = prepare_http_headers(options.delete(:headers) || {}).
|
83
|
+
merge('async.callback' => callback).
|
84
|
+
merge(params ? {:params => params} : {})
|
85
|
+
timeout_secs = options.delete(:timeout) || default_timeout
|
86
|
+
begin
|
87
|
+
timeout(timeout_secs) do
|
88
|
+
EM.run do
|
89
|
+
catch(:async) do
|
90
|
+
result = @request.send(method, path, headers)
|
91
|
+
callback.call([result.status, result.header, "Something went wrong"])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
rescue Timeout::Error => e
|
96
|
+
raise Timeout::Error.new(e.message + " (No render call in action?)")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def sync_request(method, path, options)
|
101
|
+
max_chunks = options.delete(:max_chunks) || 1
|
102
|
+
response = nil
|
103
|
+
async_request(method, path, options) do |result|
|
104
|
+
if result.status.between?(200, 299)
|
105
|
+
result.read_body(max_chunks)
|
106
|
+
else
|
107
|
+
EM.next_tick { EM.stop }
|
108
|
+
end
|
109
|
+
response = result
|
110
|
+
end
|
111
|
+
response
|
112
|
+
end
|
113
|
+
|
114
|
+
def parse_response(block)
|
115
|
+
proc do |result|
|
116
|
+
response = MockResponse.new(result)
|
117
|
+
block.call(response)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def prepare_http_headers(headers)
|
122
|
+
headers.inject({}) {|acc, (k,v)| acc["HTTP_#{k.upcase.gsub("-", "_")}"] = v; acc}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|