I have a controller that i want to write rspec for
results_controller.rb
class Api::V1::ResultsController < Api::V1::ApplicationController
before_action :devices
include DataHelper
def show
results = get_dr_results
render json: { data: results }
end
private
def get_dr_results
program_ids = method_defined_in_crucible_helper
end
end
module DataHelper
include Cruciblehelper
def method_missing(method_name, *args, &block)
if condition
do_something
else
super.method_missing(method_name, *args, &block)
end
end
def respond_to_missing?
true
end
end
module CrucibleHelper
def method_defined_in_crucible_helper
end
end
Now in my rspec, I try to mock the method method_defined_in_crucible_helper.
describe Api::V1::DrResultsController, type: :controller do
describe 'GET #show' do
before do
allow_any_instance_of(CrucibleHelper).to receive(:method_defined_in_crucible_helper) { [utility_program.id, utility_program2.id] }
end
context 'returns data' do
context 'returns expected events' do
it 'should return success response with expected events' do
get :show
expect(JSON.parse(response.body)).to eq(expected_response)
end
end
I am getting
Failure/Error:
def respond_to_missing?
true
end
ArgumentError:
wrong number of arguments (given 2, expected 0)
# ./app/helpers/data_helper.rb:72:in `respond_to_missing?'
If I comment out respond_to_missing? method, then my specs are executing OK. Can someone help me in fixing this error?
Ruby Delegator#respond_to_missing? is method take responsible for returning whether a missing method be able to handled by the object or not, it takes 2 parameters: the missing method name and the option include_private.
The best practice is: always define respond_to_missing? when overriding method_missing.
However i do not prefer the way you applied, the reason behind that is The Rule of Least Surprise, take a look:
class DataHelper
def method_missing(method_name, *args, &block)
if method_name.to_s.start_with?('delegate')
puts "a delegate method"
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
true
end
end
d = DataHelper.new
d.respond_to?(:answer) # true
d.answer # `method_missing': undefined method `answer' ... SURPRISE
as you can see, d response that he can responsible for the answer method but when call that method, a method_missing error be raised.
So, you need to make both method_missing and respond_to_missing? match together:
class DataHelper
def method_missing(method_name, *args, &block)
if can_handle?(method_name)
puts "a delegate method"
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
return true if can_handle?(method_name)
super
end
private
def can_handle?(method_name)
method_name.to_s.start_with?('delegate')
end
end
d = D.new
d.respond_to?(:delegate_answer) # true
d.delegate_answer # delegate method
d.respond_to?(:answer) # false
d.answer # error
Related
In application_controller I've got two methods which results I want to test in a maintenance_mode_controller_specs. How to create mock maintenance_mode_active? which will return false to use it inside check_maintenance??
application_controller.rb
before_action :check_maintenance?
private
def check_maintenance?
if maintenance_mode_active? == true
redirect_to maintenance_mode
elsif request.fullpath.include?(maintenance_mode_path)
redirect_to :root
end
end
def maintenance_mode_active?
# do sth ...
mode.active?
end
maintenance_mode_controller_spec.rb
context 'when maintenance mode is active' do
let(:maintenance_mode?) { instance_double(ApplicationController) }
before do
allow(ApplicationController).to receive(:maintenance_mode_active?).and_return(false)
end
it 'redirect to root path' do
expect(described_class).should redirect_to(maintenance_mode_path)
end
end
maintenance_mode_active is an instance method and you stub it on the class level. You need to use allow_any_instance_of
before do
allow_any_instance_of(ApplicationController).to receive(:maintenance_mode_active?).and_return(false)
end
Method super calls parent method based on __method__.
How to call parent method based on __callee__?
class SomeLogic
DICTIONARY = {
new_method_1: 'dictionary value 1',
new_method_2: 'dictionary value 2'
}
def initialize(method_name)
#method_name = method_name
end
def call
DICTIONARY[#method_name]
end
end
module M
extend ActiveSupport::Concern
def base_method
logic = SomeLogic.new(__callee__).call
if logic
logic
elsif defined?(__callee__)
super # here analogue of super needed
else
{}
end
end
alias_method :new_method_1, :base_method
alias_method :new_method_2, :base_method
alias_method :new_method_3, :base_method
end
class A
prepend M
def new_method_3
'foo'
end
end
Expected results:
A.new.new_method_1 # dictionary value 1
A.new.new_method_2 # dictionary value 2
A.new.new_method_3 # foo
Current results:
A.new.new_method_1 # dictionary value 1
A.new.new_method_2 # dictionary value 2
A.new.new_method_3 # no superclass method `base_method' for A
If you change
class A
prepend M
to
class A
include M
you will get the desired output
Okay so this is extremely convoluted but it will work as requested
class SomeLogic
DICTIONARY = {
new_method_1: 'dictionary value 1',
new_method_2: 'dictionary value 2'
}
def initialize(method_name)
#method_name = method_name
end
def call
DICTIONARY[#method_name]
end
end
module M
def self.prepended(base)
base.extend(ClassMethods)
end
def base_method(&block)
SomeLogic.new(__callee__).call ||
block&.call || # ruby < 2.3 block && block.call
{}
end
module ClassMethods
def method_added(method_name)
if M.instance_methods.include?(method_name)
orig = M.instance_method(method_name)
M.remove_method(method_name)
new = self.instance_method(method_name)
self.prepend(Module.new do
define_method(method_name) do
result = orig.bind(self).call(&new.bind(self))
end
end)
M.define_method(method_name, &orig.bind(M))
end
end
end
alias_method :new_method_1, :base_method
alias_method :new_method_2, :base_method
alias_method :new_method_3, :base_method
alias_method :new_method_4, :base_method
end
class A
prepend M
def new_method_3
'foo'
end
end
class B
prepend M
end
So when you define a new method we check to see if that method is already defined by M. If so then we capture that UnboundMethod remove the method from M, then we capture the new definition as an UnboundMethod then define the new method in an anonymous Module as Ms implementation passing the new method implementation as a proc to Ms implementation and prepend this module to the defining Class, then we place Ms implementation back into M so other classes still work.
Result:
p A.new.new_method_1
#=> 'dictionary value 1'
p A.new.new_method_2
#=> 'dictionary value 2'
p A.new.new_method_3
#=> 'foo'
p A.new.new_method_4
#=> {}
p B.new.new_method_3
#=> {}
If you would prefer include over prepend then M could look like
module M
def self.included(base)
super
base.extend(ClassMethods)
end
def base_method
if logic = SomeLogic.new(__callee__).call
logic
elsif self.class.instance_methods(false).include?(__callee__)
send(__callee__,true)
else
{}
end
end
module ClassMethods
def method_added(method_name)
return if #_adding_method # avoid stack level issues
if M.instance_methods.include?(method_name)
new = instance_method(method_name)
#_adding_method = true # avoid stack level issues
define_method(method_name) do |bypass_parent=false|
return super() unless bypass_parent
new.bind(self).call
end
#_adding_method = false
end
end
end
end
I have Memoize module, that provides methods for caching of class and instance methods.
module Memoize
def instance_memoize(*methods)
memoizer = Module.new do
methods.each do |method|
define_method method do
#_memoized_results ||= {}
if #_memoized_results.include? method
#_memoized_results[method]
else
#_memoized_results[method] = super()
end
end
end
end
prepend memoizer
end
def class_memoize(*methods)
methods.each do |method|
define_singleton_method method do
#_memoized_results ||= {}
if #_memoized_results.include? method
#_memoized_results[method]
else
#_memoized_results[method] = super()
end
end
end
end
end
This is an example of how I use it:
class Foo
extend Memoize
instance_memoize :instance_method1, :instance_method2
class_memoize :class_method1, :class_method2
...
end
Please advice how to avoid code duplication in this module.
One might define a lambda:
λ = lambda do
#_memoized_results ||= {}
if #_memoized_results.include? method
#_memoized_results[method]
else
#_memoized_results[method] = super()
end
end
And. then:
define_method method, &λ
Please be aware of an ampersand in front of λ, it is used to notice define_method about it’s receiving a block, rather than a regular argument.
I did not get what failed with this approach on your side, but here is a bullet-proof version:
method_declaration = %Q{
def %{method}
#_memoized_results ||= {}
if #_memoized_results.include? :%{method}
#_memoized_results[:%{method}]
else
#_memoized_results[:%{method}] = super()
end
end
}
methods.each do |method|
class_eval method_declaration % {method: method}
end
I've entirely reworded this question as I feel this more accurately reflects what I wanted to ask the first time in a less roundabout way.
After instantiating a FormObject, calls to dynamically defined methods do not evaluate their block parameter in the context I'm trying for. For example:
#registration = RegistrationForm.new
#registration.user
# undefined local variable or method `user_params' for RegistrationForm:Class
RegistrationForm calls a class method exposing(:user) { User.new(user_params) } which I would like to have define a new method that looks like this:
def user
#user ||= User.new(user_params)
end
My implementation doesn't use #ivar ||= to cache the value (since falsey values will cause the method to be re-evaluated). I borrowed the idea from rspec's memoized_helpers and I 'think' I understand how it works. What I don't understand is what I should replace class_eval with in lib/form_object/memoized_helpers.rb.
Thank you
lib/form_object/base.rb
class FormObject::Base
include ActiveModel::Model
include FormObject::MemoizedHelpers
attr_reader :params, :errors
def initialize(params = {})
#params = ActionController::Parameters.new(params)
#errors = ActiveModel::Errors.new(self)
end
def save
valid? && persist
end
end
lib/form_object/memoized_helpers.rb
module FormObject
module MemoizedHelpers
private
def __memoized
#__memoized ||= {}
end
def self.included(mod)
mod.extend(ClassMethods)
end
module ClassMethods
def exposing(name, &block)
raise "#exposing called without a block" unless block_given?
class_eval do
define_method(name) { __memoized.fetch(name) { |k| __memoized[k] = block.call } }
end
end
end
end
end
app/forms/registration_form.rb
class RegistrationForm < FormObject::Base
exposing(:user) { User.new(user_params) { |u| u.is_admin = true } }
exposing(:tenant) { user.build_tenant(tenant_params) }
validate do
tenant.errors.each do |key, value|
errors.add("#{tenant.class.name.underscore}_#{key}", value)
end unless tenant.valid?
end
validate do
user.errors.each do |key, value|
errors.add("#{user.class.name.underscore}_#{key}", value)
end unless user.valid?
end
private
def persist
user.save
end
def user_params
params.fetch(:user, {}).permit(:first_name, :last_name, :email, :password, :password_confirmation)
end
def tenant_params
params.fetch(:tenant, {}).permit(:name)
end
end
So, I might have simplified this example too much, but I think this is what you want:
module Exposing
def exposing(name, &block)
instance_eval do
define_method(name, block)
end
end
end
class Form
extend Exposing
exposing(:user) { user_params }
def user_params
{:hi => 'ho'}
end
end
Form.new.user
You can fiddle around here: http://repl.it/OCa
My current code:
class Product < ActiveRecord::Base
belongs_to :category
end
class Category < ActiveRecord::Base
def method_missing name
true
end
end
Category.new.ex_undefined_method #=> true
Product.last.category.ex_undefined_method #=> NoMethodError: undefined method `ex_undefined_method' for #<ActiveRecord::Associations::BelongsToAssociation:0xc4cd52c>
This happens because of this code in rails which only passes methods that exist to the model.
private
def method_missing(method, *args)
if load_target
if #target.respond_to?(method)
if block_given?
#target.send(method, *args) { |*block_args| yield(*block_args) }
else
#target.send(method, *args)
end
else
super
end
end
end
This is what I want:
Product.last.category.ex_undefined_method #=> true
How can I accomplish this?
Note that the AssociationProxy object only sends on methods that the target claims to respond_to?. Therefore, the fix here is to update respond_to? as well:
class Category < ActiveRecord::Base
def method_missing(name, *args, &block)
if name =~ /^handleable/
"Handled"
else
super
end
end
def respond_to?(name)
if name =~ /^handleable/
true
else
super
end
end
end
In fact, you should always update respond_to? if you redefine method_missing - you've changed the interface of your class, so you need to make sure that everyone knows about it. See here.
Chowlett's response is indeed the way to go imho.
But, if you are using Rails 3*, make sure to include the second argument that has been introduced in the responds_to? definition:
def respond_to?(name,include_private = false)
if name =~ /^handleable/
true
else
super
end
end
Replace
if #target.respond_to?(method)
if block_given?
#target.send(method, *args) { |*block_args| yield(*block_args) }
else
#target.send(method, *args)
end
else
super
end
by
if block_given?
#target.send(method, *args) { |*block_args| yield(*block_args) }
else
#target.send(method, *args)
end
As monkey patch to the AssociationProxy