eol_rackbox 1.1.7
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/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
|