rspec-crampy 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec'
4
+ require 'rspec/core'
5
+ require 'rspec/core/rake_task'
6
+
7
+
8
+ RSpec::Core::RakeTask.new(:spec) do |spec|
9
+ spec.pattern = FileList['spec/**/*_spec.rb']
10
+ end
11
+
12
+ task :default => :spec
@@ -0,0 +1,7 @@
1
+ require 'cramp'
2
+
3
+ require 'rspec'
4
+ require 'rspec/cramp/extensions/cramp/action'
5
+ require 'rspec/cramp/matchers/respond_with'
6
+ require 'rspec/cramp/mock_response'
7
+ require 'rspec/cramp/shared_context'
@@ -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