eol_rackbox 1.1.7
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/RDOC_README.rdoc +88 -0
- data/README.markdown +134 -0
- data/README.rdoc +17 -0
- data/Rakefile +59 -0
- data/VERSION.yml +5 -0
- data/bin/rackbox +4 -0
- data/lib/eol_rackbox.rb +18 -0
- data/lib/rackbox/app.rb +30 -0
- data/lib/rackbox/bacon.rb +1 -0
- data/lib/rackbox/bin.rb +183 -0
- data/lib/rackbox/matchers.rb +30 -0
- data/lib/rackbox/rack/content_length_fix.rb +19 -0
- data/lib/rackbox/rack/extensions_for_rspec.rb +33 -0
- data/lib/rackbox/rack/sticky_sessions.rb +54 -0
- data/lib/rackbox/rackbox.rb +186 -0
- data/lib/rackbox/spec.rb +10 -0
- data/lib/rackbox/spec/configuration.rb +75 -0
- data/lib/rackbox/spec/helpers.rb +23 -0
- data/lib/rackbox/test.rb +1 -0
- data/lib/rspec/custom_matcher.rb +52 -0
- data/rails_generators/blackbox_spec/USAGE +5 -0
- data/rails_generators/blackbox_spec/blackbox_spec_generator.rb +56 -0
- data/rails_generators/blackbox_spec/templates/spec.erb +9 -0
- data/spec/basic_auth_spec.rb +29 -0
- data/spec/custom_request_header_specs.rb +14 -0
- data/spec/posting_data_spec.rb +33 -0
- data/spec/rackbox_build_query_spec.rb +35 -0
- data/spec/request_method_spec.rb +23 -0
- data/spec/spec_helper.rb +1 -0
- metadata +103 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
class RackBox
|
2
|
+
|
3
|
+
# Custom RSpec matchers
|
4
|
+
module Matchers
|
5
|
+
|
6
|
+
def self.included base
|
7
|
+
|
8
|
+
# this should really just be matcher(:foo){ ... }
|
9
|
+
# but there's a bit of other meta logic to deal with here
|
10
|
+
Object.send :remove_const, :RedirectTo if defined? RedirectTo
|
11
|
+
undef redirect_to if defined? redirect_to
|
12
|
+
|
13
|
+
# the actual matcher logic
|
14
|
+
matcher(:redirect_to, base) do |response, url|
|
15
|
+
return false unless response['Location']
|
16
|
+
if url =~ /^\//
|
17
|
+
# looking for a relative match, eg. should redirect_to('/login')
|
18
|
+
relative_location = response['Location'].sub(/^https?:\/\//,'').sub(/^[^\/]*/,'')
|
19
|
+
# ^ there's probably a helper on Rack or CGI to do this
|
20
|
+
relative_location.downcase == url.downcase
|
21
|
+
else
|
22
|
+
response['Location'].downcase == url.downcase
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# An evil fix
|
2
|
+
#
|
3
|
+
# The actual fix
|
4
|
+
# has been pulled upstream into Rack
|
5
|
+
# but hasn't made it into the Rack gem yet, so we need this fix until the new gem is released
|
6
|
+
#
|
7
|
+
class Rack::MockRequest
|
8
|
+
class << self
|
9
|
+
|
10
|
+
alias env_for_without_content_length_fix env_for
|
11
|
+
def env_for_with_content_length_fix uri = '', opts = {}
|
12
|
+
env = env_for_without_content_length_fix uri, opts
|
13
|
+
env['CONTENT_LENGTH'] ||= env['rack.input'].length
|
14
|
+
env
|
15
|
+
end
|
16
|
+
alias env_for env_for_with_content_length_fix
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# some more Rack extensions to help when testing
|
2
|
+
class Rack::MockResponse
|
3
|
+
|
4
|
+
# TODO checkout Rack::Response::Helpers which implements many of these!
|
5
|
+
|
6
|
+
# these methods help with RSpec specs so we can ask things like:
|
7
|
+
#
|
8
|
+
# request('/').should be_successful
|
9
|
+
# request('/').should be_redirect
|
10
|
+
# request('/').should be_error
|
11
|
+
#
|
12
|
+
|
13
|
+
def success?
|
14
|
+
self.status.to_s.start_with?'2' # 200 status codes are successful
|
15
|
+
end
|
16
|
+
|
17
|
+
def redirect?
|
18
|
+
self.status.to_s.start_with?'3' # 300 status codes are redirects
|
19
|
+
end
|
20
|
+
|
21
|
+
def client_error?
|
22
|
+
self.status.to_s.start_with?'4' # 400 status codes are client errors
|
23
|
+
end
|
24
|
+
|
25
|
+
def server_error?
|
26
|
+
self.status.to_s.start_with?'5' # 500 status codes are server errors
|
27
|
+
end
|
28
|
+
|
29
|
+
def error?
|
30
|
+
self.status.to_s.start_with?('4') || self.status.to_s.start_with?('5') # 400 & 500 status codes are errors
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
#
|
2
|
+
# little extension to Rack::MockRequest to track cookies
|
3
|
+
#
|
4
|
+
class Rack::MockRequest
|
5
|
+
# cookies is a hash of persistent cookies (by domain)
|
6
|
+
# that let you test cookies for your app
|
7
|
+
#
|
8
|
+
# cookies = {
|
9
|
+
# 'example.org' => {
|
10
|
+
# 'cookie-name' => 'cookie-value',
|
11
|
+
# 'chunky' => 'bacon'
|
12
|
+
# }
|
13
|
+
# }
|
14
|
+
attr_accessor :cookies
|
15
|
+
|
16
|
+
# shortcut to get cookies for a particular domain
|
17
|
+
def cookies_for domain
|
18
|
+
@cookies ||= {}
|
19
|
+
@cookies[ domain ]
|
20
|
+
end
|
21
|
+
|
22
|
+
# oh geez ... it looks like i basically copy/pasted this. there's gotta be a way to do this that's
|
23
|
+
# more resilient to Rack changes to this method. i don't like overriding the whole method!
|
24
|
+
#
|
25
|
+
def request method = "GET", uri = "", opts = { }
|
26
|
+
|
27
|
+
env = self.class.env_for(uri, opts.merge(:method => method))
|
28
|
+
|
29
|
+
unless @cookies.nil? or @cookies.empty? or @cookies[env['SERVER_NAME']].nil? or @cookies[env['SERVER_NAME']].empty?
|
30
|
+
env['HTTP_COOKIE'] = @cookies[env['SERVER_NAME']].map{ |k,v| "#{ k }=#{ v }" }.join('; ')
|
31
|
+
end
|
32
|
+
|
33
|
+
if opts[:lint]
|
34
|
+
app = Rack::Lint.new(@app)
|
35
|
+
else
|
36
|
+
app = @app
|
37
|
+
end
|
38
|
+
|
39
|
+
errors = env["rack.errors"]
|
40
|
+
response = Rack::MockResponse.new(*(app.call(env) + [errors]))
|
41
|
+
|
42
|
+
if response.original_headers['Set-Cookie']
|
43
|
+
@cookies ||= {}
|
44
|
+
@cookies[ env['SERVER_NAME'] ] ||= {}
|
45
|
+
response.headers['Set-Cookie'].map{ |str| /(.*); path/.match(str)[1] }.each do |cookie|
|
46
|
+
name, value = cookie.split('=').first, cookie.split('=')[1]
|
47
|
+
@cookies[ env['SERVER_NAME'] ][ name ] = value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
response
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
|
2
|
+
# To add blackbox testing to a Rails app,
|
3
|
+
# in your spec_helper.rb
|
4
|
+
#
|
5
|
+
# require 'rackbox'
|
6
|
+
#
|
7
|
+
# Spec::Runner.configure do |config|
|
8
|
+
# config.use_blackbox = true
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
class RackBox
|
12
|
+
|
13
|
+
# i am an rdoc comment on RackBox's eigenclass
|
14
|
+
class << self
|
15
|
+
|
16
|
+
# to turn on some verbosity / logging, set:
|
17
|
+
# RackBox.verbose = true
|
18
|
+
attr_accessor :verbose
|
19
|
+
|
20
|
+
# A port of Merb's request() method, used in tests
|
21
|
+
#
|
22
|
+
# At the moment, we're using #req instead because #request conflicts
|
23
|
+
# with an existing RSpec-Rails method
|
24
|
+
#
|
25
|
+
# Usage:
|
26
|
+
#
|
27
|
+
# req '/'
|
28
|
+
# req login_path
|
29
|
+
# req url_for(:controller => 'login')
|
30
|
+
#
|
31
|
+
# req '/', :method => :post, :params => { 'chunky' => 'bacon' }
|
32
|
+
#
|
33
|
+
# req '/', :data => "some XML data to POST"
|
34
|
+
#
|
35
|
+
# TODO take any additional options and pass them along to the environment, so we can say
|
36
|
+
# req '/', :user_agent => 'some custom user agent'
|
37
|
+
#
|
38
|
+
def req app_or_request, url = nil, options = {}
|
39
|
+
puts "RackBox#request url:#{ url.inspect }, options:#{ options.inspect }" if RackBox.verbose
|
40
|
+
|
41
|
+
# handle RackBox.request '/foo'
|
42
|
+
if app_or_request.is_a?(String) && ( url.nil? || url.is_a?(Hash) )
|
43
|
+
options = url || {}
|
44
|
+
url = app_or_request
|
45
|
+
app_or_request = RackBox.app
|
46
|
+
end
|
47
|
+
|
48
|
+
# need to find the request or app
|
49
|
+
mock_request = nil
|
50
|
+
if app_or_request.is_a? Rack::MockRequest
|
51
|
+
mock_request = app_or_request
|
52
|
+
elsif app_or_request.nil?
|
53
|
+
if RackBox.app.nil?
|
54
|
+
raise "Not sure howto to execute a request against app or request: #{ app_or_request.inspect }"
|
55
|
+
else
|
56
|
+
mock_request = Rack::MockRequest.new(RackBox.app) # default to RackBox.app if nil
|
57
|
+
end
|
58
|
+
elsif app_or_request.respond_to? :call
|
59
|
+
mock_request = Rack::MockRequest.new(app_or_request)
|
60
|
+
else
|
61
|
+
raise "Not sure howto to execute a request against app or request: #{ app_or_request.inspect }"
|
62
|
+
end
|
63
|
+
|
64
|
+
options[:method] ||= ( options[:params] || options[:data] ) ? :post : :get # if params, default to POST, else default to GET
|
65
|
+
options[:params] ||= { }
|
66
|
+
|
67
|
+
if options[:data]
|
68
|
+
# input should be the data we're likely POSTing ... this overrides any params
|
69
|
+
input = options[:data]
|
70
|
+
else
|
71
|
+
# input should be params, if any
|
72
|
+
input = RackBox.build_query options[:params]
|
73
|
+
end
|
74
|
+
|
75
|
+
# add HTTP BASIC AUTH support
|
76
|
+
#
|
77
|
+
# TODO: DRY this up!
|
78
|
+
#
|
79
|
+
if options[:auth]
|
80
|
+
options[:http_basic_authentication] = options[:auth]
|
81
|
+
options.delete :auth
|
82
|
+
end
|
83
|
+
if options[:basic_auth]
|
84
|
+
options[:http_basic_authentication] = options[:basic_auth]
|
85
|
+
options.delete :basic_auth
|
86
|
+
end
|
87
|
+
if options[:http_basic_authentication]
|
88
|
+
username, password = options[:http_basic_authentication]
|
89
|
+
options.delete :http_basic_authentication
|
90
|
+
require 'base64'
|
91
|
+
# for some reason, nase64 encoding adds a \n
|
92
|
+
encoded_username_and_password = Base64.encode64("#{ username }:#{ password }").sub(/\n$/, '')
|
93
|
+
options['HTTP_AUTHORIZATION'] = "Basic #{ encoded_username_and_password }"
|
94
|
+
end
|
95
|
+
|
96
|
+
headers = options.dup
|
97
|
+
headers.delete :data if headers[:data]
|
98
|
+
headers.delete :params if headers[:params]
|
99
|
+
headers.delete :method if headers[:method]
|
100
|
+
|
101
|
+
# merge input
|
102
|
+
headers[:input] = input
|
103
|
+
|
104
|
+
puts " requesting #{ options[:method].to_s.upcase } #{ url.inspect } #{ headers.inspect }" if RackBox.verbose
|
105
|
+
mock_request.send options[:method], url, headers
|
106
|
+
end
|
107
|
+
|
108
|
+
alias request req unless defined? request
|
109
|
+
|
110
|
+
# the Rack appliction to do 'Black Box' testing against
|
111
|
+
#
|
112
|
+
# To set, in your spec_helper.rb or someplace:
|
113
|
+
# RackBox.app = Rack::Adapter::Rails.new :root => '/root/directory/of/rails/app', :environment => 'test'
|
114
|
+
#
|
115
|
+
# If not explicitly set, uses RAILS_ROOT (if defined?) and RAILS_ENV (if defined?)
|
116
|
+
attr_accessor :app
|
117
|
+
|
118
|
+
def app options = { }
|
119
|
+
unless @app and @app.respond_to?:call
|
120
|
+
|
121
|
+
options = {
|
122
|
+
:silent => false
|
123
|
+
}.merge(options)
|
124
|
+
|
125
|
+
if File.file? 'config.ru'
|
126
|
+
@app = Rack::Builder.new { eval(File.read('config.ru')) }
|
127
|
+
|
128
|
+
elsif defined?RAILS_ENV and defined?RAILS_ROOT
|
129
|
+
unless defined?Rack::Adapter::Rails
|
130
|
+
# TODO this is no longer true ... right? does rails < 2.3 work without Thin
|
131
|
+
puts "You need the Rack::Adapter::Rails to run Rails apps with RackBox." +
|
132
|
+
" Try: sudo gem install thin" unless options[:silent]
|
133
|
+
else
|
134
|
+
@app = Rack::Adapter::Rails.new :root => RAILS_ROOT, :environment => RAILS_ENV
|
135
|
+
end
|
136
|
+
|
137
|
+
elsif File.file?('config/routes.rb') && File.file?('config/environment.rb')
|
138
|
+
unless defined?Rack::Adapter::Rails
|
139
|
+
puts "You need the Rack::Adapter::Rails to run Rails apps with RackBox." +
|
140
|
+
" Try: sudo gem install thin" unless options[:silent]
|
141
|
+
else
|
142
|
+
@app = Rack::Adapter::Rails.new :root => '.', :environment => 'development'
|
143
|
+
end
|
144
|
+
|
145
|
+
else
|
146
|
+
puts "RackBox.app not configured." unless options[:silent]
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
@app
|
152
|
+
end
|
153
|
+
|
154
|
+
# helper method for taking a Hash of params and turning them into POST params
|
155
|
+
#
|
156
|
+
# >> RackBox.build_query :hello => 'there'
|
157
|
+
# => 'hello=there'
|
158
|
+
#
|
159
|
+
# >> RackBox.build_query :hello => 'there', :foo => 'bar'
|
160
|
+
# => 'hello=there&foo=bar'
|
161
|
+
#
|
162
|
+
# >> RackBox.build_query :user => { :name => 'bob', :password => 'secret' }
|
163
|
+
# => 'user[name]=bob&user[password]=secret'
|
164
|
+
#
|
165
|
+
def build_query params_hash = { }
|
166
|
+
# check to make sure no values are Hashes ...
|
167
|
+
# if they are, we need to flatten them!
|
168
|
+
params_hash.each do |key, value|
|
169
|
+
# params_hash :a => { :b => X, :c => Y }
|
170
|
+
# needs to be 'a[b]' => X, 'a[b]' => Y
|
171
|
+
if value.is_a? Hash
|
172
|
+
inner_hash = params_hash.delete key # { :b => X, :c => Y }
|
173
|
+
inner_hash.each do |subkey, subvalue|
|
174
|
+
new_key = "#{ key }[#{ subkey }]" # a[b] or a[c]
|
175
|
+
puts "warning: overwriting query parameter #{ new_key }" if params_hash[new_key]
|
176
|
+
params_hash[new_key] = subvalue # 'a[b]' => X or a[c] => Y
|
177
|
+
end
|
178
|
+
# we really shouldn't keep going thru the #each now that we've altered data!
|
179
|
+
return build_query(params_hash)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
Rack::Utils.build_query params_hash
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
end
|
data/lib/rackbox/spec.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# this should get you up and running for using RackBox with RSpec
|
2
|
+
require File.dirname(__FILE__) + '/../rackbox'
|
3
|
+
|
4
|
+
spec_configuration = nil
|
5
|
+
spec_configuration = Spec::Example if defined? Spec::Example
|
6
|
+
spec_configuration = Spec::Runner if defined? Spec::Runner
|
7
|
+
|
8
|
+
spec_configuration.configure do |config|
|
9
|
+
config.use_blackbox = true
|
10
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# Extend the RSpec configuration class with a use_blackbox option
|
2
|
+
#
|
3
|
+
# To add blackbox testing to a Rails app,
|
4
|
+
# in your spec_helper.rb
|
5
|
+
#
|
6
|
+
# require 'rackbox'
|
7
|
+
#
|
8
|
+
# Spec::Runner.configure do |config|
|
9
|
+
# config.use_blackbox = true
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
|
13
|
+
spec_configuration_class = nil
|
14
|
+
spec_configuration_class = Spec::Example::Configuration if defined? Spec::Example::Configuration
|
15
|
+
spec_configuration_class = Spec::Runner::Configuration if defined? Spec::Runner::Configuration
|
16
|
+
|
17
|
+
if spec_configuration_class
|
18
|
+
spec_configuration_class.class_eval do
|
19
|
+
# Adds blackbox testing to your Rails application using RackBox.
|
20
|
+
#
|
21
|
+
# To use, put your 'blackbox' specs into the spec/blackbox
|
22
|
+
# directory, eg. spec/blackbox/login_spec.rb
|
23
|
+
#
|
24
|
+
# In these specs, the RackBox::SpecHelpers#req method will be available to you
|
25
|
+
#
|
26
|
+
def use_blackbox= bool
|
27
|
+
if bool == true
|
28
|
+
|
29
|
+
before(:all, :type => :blackbox) do
|
30
|
+
self.class.instance_eval {
|
31
|
+
# include our own helpers, eg. RackBox::SpecHelpers#req
|
32
|
+
include RackBox::SpecHelpers
|
33
|
+
include RackBox::Matchers
|
34
|
+
|
35
|
+
# include generated url methods, eg. login_path.
|
36
|
+
# default_url_options needs to have a host set for the Urls to work
|
37
|
+
if defined?ActionController::UrlWriter
|
38
|
+
include ActionController::UrlWriter
|
39
|
+
default_url_options[:host] = 'example.com'
|
40
|
+
end
|
41
|
+
|
42
|
+
# if we're not in a Rails app, let's try to load matchers from Webrat
|
43
|
+
unless defined?RAILS_ENV
|
44
|
+
begin
|
45
|
+
require 'webrat'
|
46
|
+
require 'webrat/core/matchers'
|
47
|
+
include Webrat::HaveTagMatcher
|
48
|
+
# include Webrat::HasContent
|
49
|
+
rescue LoadError
|
50
|
+
puts "Webrat not available. have_tag & other matchers won't be available. to install, sudo gem install webrat"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_accessor :rackbox_request
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
before(:each, :type => :blackbox) do
|
59
|
+
|
60
|
+
# i'm sure there's a better way to write this!
|
61
|
+
#
|
62
|
+
# i believe metaid would write this as:
|
63
|
+
# metaclass.class_eval do ... end
|
64
|
+
#
|
65
|
+
(class << self; self; end).class_eval do
|
66
|
+
include RackBox::Matchers
|
67
|
+
end
|
68
|
+
|
69
|
+
@rackbox_request = Rack::MockRequest.new RackBox.app
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class RackBox
|
2
|
+
|
3
|
+
# Helper methods to include in specs that want to use blackbox testing
|
4
|
+
#
|
5
|
+
# TODO For backwards compatibility, I would like to keep a SpecHelpers
|
6
|
+
# module, but this needs to be renamed because this isn't spec
|
7
|
+
# specific at all! it needs to be easy to RackBox::App.new(rack_app).request
|
8
|
+
# or something like that (something generic)
|
9
|
+
#
|
10
|
+
# This module has the RackBox::SpecHelpers#request method, which is
|
11
|
+
# the main method used by RackBox blackbox tests
|
12
|
+
#
|
13
|
+
module SpecHelpers
|
14
|
+
|
15
|
+
# moved logic into RackBox#request, where it can easily be re-used
|
16
|
+
def req url, options = {}
|
17
|
+
RackBox.request @rackbox_request, url, options
|
18
|
+
end
|
19
|
+
|
20
|
+
alias request req unless defined? request
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
data/lib/rackbox/test.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# this should get you up and running for using RackBox with test/unit
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# created by: http://github.com/xdotcommer
|
2
|
+
# from: http://github.com/xdotcommer/rspec-custom-matchers/blob/0ecfccd659d5038cdfc88fdc1fee08373e1ee75c/custom_matcher.rb
|
3
|
+
class CustomMatcher
|
4
|
+
def self.create(class_name, &block)
|
5
|
+
klass = Class.new(CustomMatcher)
|
6
|
+
klass.send(:define_method, :matcher, &block) if block_given?
|
7
|
+
Object.const_set(build_class_name(class_name), klass)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(expected = nil)
|
11
|
+
@expected = expected
|
12
|
+
end
|
13
|
+
|
14
|
+
def failure_message
|
15
|
+
message
|
16
|
+
end
|
17
|
+
|
18
|
+
def negative_failure_message
|
19
|
+
message(false)
|
20
|
+
end
|
21
|
+
|
22
|
+
def matcher(target, expected)
|
23
|
+
target == expected
|
24
|
+
end
|
25
|
+
|
26
|
+
def matches?(target)
|
27
|
+
@target = target
|
28
|
+
if self.method(:matcher).arity == 2
|
29
|
+
matcher(@target, @expected)
|
30
|
+
else
|
31
|
+
matcher(@target)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def message(positive = true)
|
37
|
+
"#{positive ? 'Expected' : 'Did not expect'} #{@target.inspect} to #{class_display_name} #{@expected.inspect if self.method(:matcher).arity == 2}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def class_display_name
|
41
|
+
self.class.to_s.gsub(/[A-Z]/) {|m| ' ' + m.downcase }.lstrip
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.build_class_name(class_name)
|
45
|
+
class_name.to_s.split('_').map {|s| s.capitalize}.join
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def matcher(name, context = self.class, &block)
|
50
|
+
klass = CustomMatcher.create(name, &block)
|
51
|
+
context.send(:define_method, name) { |*args| klass.new(*args) }
|
52
|
+
end
|