Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Created gem version 0.1.0. I reorganized the files so the gem is easi…

…er to use

and updated the README.
  • Loading branch information...
commit b8f2ebf91d7733c94074a052f964b3e03dd72ac5 1 parent cf685c0
Martin Bilski authored
View
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2011 Martin Bilski
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
9 README.md
@@ -3,6 +3,8 @@ A set of rspec matchers and helpers that make it easier to write specs for [cram
Quick start
-----------
+ require 'rspec/cramp'
+
describe HelloWorld, :cramp => true do
def app
HelloWorld
@@ -39,10 +41,11 @@ The matcher is fairly flexible, supports regular expressions and also works with
Project status
--------------
-**IMPORTANT:** This is work in progress. I haven't created a gem yet.
+**IMPORTANT:** This is work in progress.
-1. There are still some things I'll take care of soon (esp. better failure messages).
-2. I extracted the code from one of my projects and rewrote the matchers from scratch test-first. Still, after the weekend I plan to actually use it to replace the 'legacy' matchers in my project; this will probably uncover some bugs and may make me add more functionality.
+1. I have created a gem and restructured the files 'a bit'. I'll publish the gem soon in its current form. Right now, you can build it using the provided gemspec.
+2. There are still some things I'll take care of soon (esp. better failure messages).
+3. I extracted the code from one of my projects and rewrote the matchers from scratch test-first. Still, after the weekend I plan to actually use it to replace the 'legacy' matchers in my project; this will probably uncover some bugs and may make me add more functionality. *UPDATE: I'm working on it right now.*
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)
View
133 lib/mock_response.rb
@@ -1,133 +0,0 @@
-module Cramp
-
- # Response from get, post etc. methods called in rspecs.
- class MockResponse
- def initialize(response)
- @status = response[0]
- @headers = response[1]
- @body = response[2]
- end
-
- def read_body(max_chunks = 1, &block)
- if @body.is_a? Cramp::Body
- stopping = false
- deferred_body = @body
- chunks = []
- deferred_body.each do |chunk|
- chunks << chunk unless stopping
- if chunks.count >= max_chunks
- @body = chunks
- stopping = true
- block.call if block
- EM.next_tick { EM.stop }
- end
- end
- end
- end
-
- def [](i)
- [@status, @headers, @body][i]
- end
-
- def body
- if @body.is_a? Cramp::Body
- raise "Error: Something went wrong or body is not loaded yet (use response.read_body do { })."
- end
- @body
- end
-
- def headers
- @headers
- end
-
- def status
- @status
- end
-
- def matching?(match_options)
- expected_status = match_options.delete(:status)
- expected_header = match_options.delete(:headers)
- expected_body = match_options.delete(:body)
- expected_chunks = match_options.delete(:chunks)
- raise "Unsupported match option" unless match_options.empty?
- matching_status?(expected_status) && matching_headers?(expected_header) && matching_body?(expected_body) &&
- matching_chunks?(expected_chunks)
- end
-
- def last_failure_message_for_should
- # TODO Better failure message showing the specific mismatches that made it fail.
- "expected #{@failure_info[:expected]} in #{@failure_info[:what].to_s} but got: #{@failure_info[:actual]}"
- end
- def last_failure_message_for_should_not
- # TODO Better failure message showing the specific successful matches that made it fail.
- "expected response not to match the conditions but got: #{[@status, @headers, @body].inspect}"
- end
-
- private
-
- def matching_response_element?(what, actual, expected)
- is_match = if expected.nil?
- true # No expectation set.
- elsif actual.nil?
- false
- elsif expected.is_a? Regexp
- actual.to_s.match(expected)
- elsif expected.is_a? Integer
- actual.to_i == expected
- elsif expected.is_a? String
- actual.to_s == expected
- else
- raise "Unsupported type"
- end
- @failure_info = is_match ? {} : {:what => what, :actual => actual, :expected => format_expected(expected)}
- is_match
- end
-
- def resolve_status(status)
- case status
- when :ok then /^2[0-9][0-9]$/
- when :error then /^[^2][0-9][0-9]$/
- else status
- end
- end
-
- def format_expected(expected)
- expected.is_a?(Regexp) ? "/#{expected.source}/" : expected.inspect
- end
-
- def matching_status?(expected_status)
- matching_response_element?(:status, @status, resolve_status(expected_status))
- end
-
- def matching_header_values?(expected_header)
- expected_header.find do |ek, ev|
- @headers.find { |ak, av| matching_response_element?(:headers, ak, ek) && !matching_response_element?(:headers, av, ev) } != nil
- end == nil
- end
-
- def matching_header_keys?(expected_header)
- is_match = @headers.keys.find do |actual|
- expected_header.keys.find {|expected| matching_response_element?(:headers, actual, expected)} != nil
- end != nil
- @failure_info = is_match ? {} : {:what => :headers, :actual => @headers.keys.inspect, :expected => expected_header.keys.inspect}
- is_match
- end
-
- def matching_headers?(expected_header)
- expected_header.nil? ||
- (matching_header_keys?(expected_header) && matching_header_values?(expected_header))
- end
-
- def matching_body?(expected_body)
- actual_body = @body.is_a?(Array) ? @body.join("") : @body
- matching_response_element?(:body, actual_body, expected_body)
- end
-
- def matching_chunks?(expected_chunks)
- expected_chunks.nil? || (@body.is_a?(Array) &&
- @body.zip(expected_chunks).find do |actual, expected|
- !matching_response_element?(:chunks, actual, expected)
- end.nil?)
- end
- end
-end
View
6 lib/rspec/cramp.rb
@@ -0,0 +1,6 @@
+require 'cramp'
+
+require 'rspec/cramp/extensions/cramp/action'
+require 'rspec/cramp/matchers/respond_with'
+require 'rspec/cramp/mock_response'
+require 'rspec/cramp/shared_context'
View
17 lib/rspec/cramp/extensions/cramp/action.rb
@@ -0,0 +1,17 @@
+# Monkey-patch so that even if there is an exception raised in on_start or in on_finish
+# the exception dump is rendered so that:
+# - you can match against it in your spec,
+# - get method doesn't time out waiting for anything to be rendered (may not be true for on_finish).
+#
+module Cramp
+ class Action
+ alias :old_handle_exception :handle_exception
+ def handle_exception(exception)
+ if @_state != :init
+ handler = ExceptionHandler.new(@env, exception)
+ render handler.pretty
+ end
+ old_handle_exception(exception)
+ end
+ end
+end
View
16 lib/rspec/cramp/matchers/respond_with.rb
@@ -0,0 +1,16 @@
+# respond_to RSpec matcher.
+# See spec/examples for sample usage.
+#
+RSpec::Matchers.define :respond_with do |options = {}|
+ match do |response|
+ @actual_response = response
+ response.matching?(options)
+ end
+
+ failure_message_for_should do
+ @actual_response.last_failure_message_for_should
+ end
+ failure_message_for_should_not do
+ @actual_response.last_failure_message_for_should_not
+ end
+end
View
135 lib/rspec/cramp/mock_response.rb
@@ -0,0 +1,135 @@
+module RSpec
+ module Cramp
+
+ # Response from get, post etc. methods called in rspecs.
+ class MockResponse
+ def initialize(response)
+ @status = response[0]
+ @headers = response[1]
+ @body = response[2]
+ end
+
+ def read_body(max_chunks = 1, &block)
+ if @body.is_a? ::Cramp::Body
+ stopping = false
+ deferred_body = @body
+ chunks = []
+ deferred_body.each do |chunk|
+ chunks << chunk unless stopping
+ if chunks.count >= max_chunks
+ @body = chunks
+ stopping = true
+ block.call if block
+ EM.next_tick { EM.stop }
+ end
+ end
+ end
+ end
+
+ def [](i)
+ [@status, @headers, @body][i]
+ end
+
+ def body
+ if @body.is_a? ::Cramp::Body
+ raise "Error: Something went wrong or body is not loaded yet (use response.read_body do { })."
+ end
+ @body
+ end
+
+ def headers
+ @headers
+ end
+
+ def status
+ @status
+ end
+
+ def matching?(match_options)
+ expected_status = match_options.delete(:status)
+ expected_header = match_options.delete(:headers)
+ expected_body = match_options.delete(:body)
+ expected_chunks = match_options.delete(:chunks)
+ raise "Unsupported match option" unless match_options.empty?
+ matching_status?(expected_status) && matching_headers?(expected_header) && matching_body?(expected_body) &&
+ matching_chunks?(expected_chunks)
+ end
+
+ def last_failure_message_for_should
+ # TODO Better failure message showing the specific mismatches that made it fail.
+ "expected #{@failure_info[:expected]} in #{@failure_info[:what].to_s} but got: #{@failure_info[:actual]}"
+ end
+ def last_failure_message_for_should_not
+ # TODO Better failure message showing the specific successful matches that made it fail.
+ "expected response not to match the conditions but got: #{[@status, @headers, @body].inspect}"
+ end
+
+ private
+
+ def matching_response_element?(what, actual, expected)
+ is_match = if expected.nil?
+ true # No expectation set.
+ elsif actual.nil?
+ false
+ elsif expected.is_a? Regexp
+ actual.to_s.match(expected)
+ elsif expected.is_a? Integer
+ actual.to_i == expected
+ elsif expected.is_a? String
+ actual.to_s == expected
+ else
+ raise "Unsupported type"
+ end
+ @failure_info = is_match ? {} : {:what => what, :actual => actual, :expected => format_expected(expected)}
+ is_match
+ end
+
+ def resolve_status(status)
+ case status
+ when :ok then /^2[0-9][0-9]$/
+ when :error then /^[^2][0-9][0-9]$/
+ else status
+ end
+ end
+
+ def format_expected(expected)
+ expected.is_a?(Regexp) ? "/#{expected.source}/" : expected.inspect
+ end
+
+ def matching_status?(expected_status)
+ matching_response_element?(:status, @status, resolve_status(expected_status))
+ end
+
+ def matching_header_values?(expected_header)
+ expected_header.find do |ek, ev|
+ @headers.find { |ak, av| matching_response_element?(:headers, ak, ek) && !matching_response_element?(:headers, av, ev) } != nil
+ end == nil
+ end
+
+ def matching_header_keys?(expected_header)
+ is_match = @headers.keys.find do |actual|
+ expected_header.keys.find {|expected| matching_response_element?(:headers, actual, expected)} != nil
+ end != nil
+ @failure_info = is_match ? {} : {:what => :headers, :actual => @headers.keys.inspect, :expected => expected_header.keys.inspect}
+ is_match
+ end
+
+ def matching_headers?(expected_header)
+ expected_header.nil? ||
+ (matching_header_keys?(expected_header) && matching_header_values?(expected_header))
+ end
+
+ def matching_body?(expected_body)
+ actual_body = @body.is_a?(Array) ? @body.join("") : @body
+ matching_response_element?(:body, actual_body, expected_body)
+ end
+
+ def matching_chunks?(expected_chunks)
+ expected_chunks.nil? || (@body.is_a?(Array) &&
+ @body.zip(expected_chunks).find do |actual, expected|
+ !matching_response_element?(:chunks, actual, expected)
+ end.nil?)
+ end
+ end
+ end
+end
View
126 lib/rspec/cramp/shared_context.rb
@@ -0,0 +1,126 @@
+module RSpec
+ module Cramp
+ # Usage:
+ #
+ # describe MyAction, :cramp => true do
+ # def app
+ # MyAction
+ # end
+ #
+ # it "should render home page" do
+ # get("/").should respond_with :status => :ok, :body => "Hello, world!"
+ # end
+ # end
+ #
+ shared_context "given a Cramp application", :cramp => true do
+
+ # In your describe block using :cramp => true, define a method called 'app' returning an async Rack application.
+ # Example:
+ #
+ # def app
+ # HelloWorldAction
+ # end
+
+ # Request helper method.
+ #
+ def request(method, path, options = {}, &block)
+ raise "Unsupported request method" unless [:get, :post, :delete, :put].include?(method)
+ if block
+ async_request(method, path, options, &block)
+ else
+ sync_request(method, path, options)
+ end
+ end
+
+ # GET helper method.
+ #
+ def get(path, options = {}, &block)
+ request(:get, path, options, &block)
+ end
+
+ # POST helper method.
+ #
+ def post(path, options = {}, &block)
+ request(:post, path, options, &block)
+ end
+
+ # DELETE helper method.
+ #
+ def delete(path, options = {}, &block)
+ request(:delete, path, options, &block)
+ end
+
+ # PUT helper method.
+ #
+ def put(path, options = {}, &block)
+ request(:put, path, options, &block)
+ end
+
+ # Use it if using a block version of a request helper method.
+ # See spec/examples/low_level_spec.rb for examples.
+ #
+ def stop
+ EM.stop
+ end
+
+ # You can change the default timeout by overriding this method.
+ #
+ def default_timeout
+ 3
+ end
+
+
+ private
+
+ before(:all) do
+ @request = Rack::MockRequest.new(app)
+ end
+
+ def async_request(method, path, options, &block)
+ callback = parse_response(block)
+ params = options.delete(:params)
+ headers = prepare_http_headers(options.delete(:headers) || {}).
+ merge('async.callback' => callback).
+ merge(params ? {:params => params} : {})
+ timeout_secs = options.delete(:timeout) || default_timeout
+ begin
+ timeout(timeout_secs) do
+ EM.run do
+ catch(:async) do
+ result = @request.send(method, path, headers)
+ callback.call([result.status, result.header, "Something went wrong"])
+ end
+ end
+ end
+ rescue Timeout::Error => e
+ raise Timeout::Error.new(e.message + " (No render call in action?)")
+ end
+ end
+
+ def sync_request(method, path, options)
+ max_chunks = options.delete(:max_chunks) || 1
+ response = nil
+ async_request(method, path, options) do |result|
+ if result.status.between?(200, 299)
+ result.read_body(max_chunks)
+ else
+ EM.next_tick { EM.stop }
+ end
+ response = result
+ end
+ response
+ end
+
+ def parse_response(block)
+ proc do |result|
+ response = MockResponse.new(result)
+ block.call(response)
+ end
+ end
+
+ def prepare_http_headers(headers)
+ headers.inject({}) {|acc, (k,v)| acc["HTTP_#{k.upcase.gsub("-", "_")}"] = v; acc}
+ end
+ end
+ end
+end
View
160 lib/rspec_cramp.rb
@@ -1,160 +0,0 @@
-require File.join(File.dirname(__FILE__), "mock_response")
-require 'cramp'
-
-module Cramp
-
- # TODO Break out into a separate file extensions/cramp/action.rb
- # Monkey-patch so that even if there is an exception raised in on_start or in on_finish
- # the exception dump is rendered so that:
- # - you can match against it in your spec,
- # - get method doesn't time out waiting for anything to be rendered (may not be true for on_finish).
- #
- class Action
- alias :old_handle_exception :handle_exception
- def handle_exception(exception)
- if @_state != :init
- handler = ExceptionHandler.new(@env, exception)
- render handler.pretty
- end
- old_handle_exception(exception)
- end
- end
-
- # Usage:
- #
- # describe MyAction, :cramp => true do
- # def app
- # MyAction
- # end
- #
- # it "should render home page" do
- # get("/").should respond_with :status => :ok, :body => "Hello, world!"
- # end
- # end
- #
- shared_context "given a Cramp application", :cramp => true do
-
- # In your describe block using :cramp => true, define a method called 'app' returning an async Rack application.
- # Example:
- #
- # def app
- # HelloWorldAction
- # end
-
- # Request helper method.
- #
- def request(method, path, options = {}, &block)
- raise "Unsupported request method" unless [:get, :post, :delete, :put].include?(method)
- if block
- async_request(method, path, options, &block)
- else
- sync_request(method, path, options)
- end
- end
-
- # GET helper method.
- #
- def get(path, options = {}, &block)
- request(:get, path, options, &block)
- end
-
- # POST helper method.
- #
- def post(path, options = {}, &block)
- request(:post, path, options, &block)
- end
-
- # DELETE helper method.
- #
- def delete(path, options = {}, &block)
- request(:delete, path, options, &block)
- end
-
- # PUT helper method.
- #
- def put(path, options = {}, &block)
- request(:put, path, options, &block)
- end
-
- # Use it if using a block version of a request helper method.
- # See spec/examples/low_level_spec.rb for examples.
- #
- def stop
- EM.stop
- end
-
- # You can change the default timeout by overriding this method.
- #
- def default_timeout
- 3
- end
-
- # TODO Break out into a separate file lib/matchers/respond_with.rb
- # respond_to RSpec matcher.
- # See spec/examples for sample usage.
- #
- RSpec::Matchers.define :respond_with do |options = {}|
- match do |response|
- @actual_response = response
- response.matching?(options)
- end
-
- failure_message_for_should do
- @actual_response.last_failure_message_for_should
- end
- failure_message_for_should_not do
- @actual_response.last_failure_message_for_should_not
- end
- end
-
- private
-
- before(:all) do
- @request = Rack::MockRequest.new(app)
- end
-
- def async_request(method, path, options, &block)
- callback = parse_response(block)
- params = options.delete(:params) || {}
- headers = prepare_http_headers(options.delete(:headers) || {}).merge('async.callback' => callback).merge(:params => params)
- timeout_secs = options.delete(:timeout) || default_timeout
- begin
- timeout(timeout_secs) do
- EM.run do
- catch(:async) do
- result = @request.send(method, path, headers)
- callback.call([result.status, result.header, "Something went wrong"])
- end
- end
- end
- rescue Timeout::Error => e
- raise Timeout::Error.new(e.message + " (No render call in action?)")
- end
- end
-
- def sync_request(method, path, options)
- max_chunks = options.delete(:max_chunks) || 1
- response = nil
- async_request(method, path, options) do |result|
- if result.status.between?(200, 299)
- result.read_body(max_chunks)
- else
- EM.next_tick { EM.stop }
- end
- response = result
- end
- response
- end
-
- def parse_response(block)
- proc do |result|
- response = MockResponse.new(result)
- block.call(response)
- end
- end
-
- def prepare_http_headers(headers)
- headers.inject({}) {|acc, (k,v)| acc["HTTP_#{k.upcase.gsub("-", "_")}"] = v; acc}
- end
- end
-end
View
21 rspec-cramp.gemspec
@@ -0,0 +1,21 @@
+Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = 'rspec-cramp'
+ s.version = '0.1.0'
+ s.summary = 'RSpec helpers for Cramp.'
+ s.description = 'RSpec extension library for Cramp.'
+
+ s.author = 'Martin Bilski'
+ s.email = 'gyamtso@gmail.com'
+ s.homepage = 'https://github.com/bilus/rspec-cramp'
+
+ s.add_dependency('cramp', '~> 0.15')
+ s.add_dependency('rack', '~> 1.3.2')
+ s.add_dependency('eventmachine', '~> 1.0.0.beta.3')
+ s.add_dependency('http_router', '~> 0.10')
+ s.add_dependency('rspec', '~> 2.6')
+ s.files = Dir['README.md', 'MIT-LICENSE', 'lib/**/*', 'spec/**/*']
+ s.has_rdoc = false
+
+ s.require_path = 'lib'
+end
View
6 spec/spec_helper.rb
@@ -2,9 +2,11 @@
require "cramp"
require "rspec"
require "http_router"
-require File.join(File.dirname(__FILE__), '../lib/rspec_cramp')
-require 'active_support/buffered_logger'
+$:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
+require "rspec/cramp"
+
+require "active_support/buffered_logger"
logger = ActiveSupport::BufferedLogger.new(File.join(File.dirname(__FILE__), "tests.log"))
logger.level = ActiveSupport::BufferedLogger::DEBUG
Cramp.logger = logger
Please sign in to comment.
Something went wrong with that request. Please try again.