一个ruby方法怎么可能不是repond_to一个方法还要调用这个方法呢?

How can a ruby method not repond_to a method but also call the method?

我正在使用一种小方法来检查租户是否启用了某项功能。每个租户都有一个存储标志的 TenantPlan。我在租户模型上也有一些标志,可用于覆盖特定于计划的标志。一个小的特征检测方法在租户模型上看起来像这样:

def has_feature?(feature, feature_default = false)
  if tenant_plan
    feature = "#{feature}_enabled?".to_sym
    return tenant_plan.send(feature) || (respond_to?(feature) and send(feature))
  end
  feature_default
end

这个问题我最感兴趣的部分是:

(respond_to?(feature) and send(feature))

我正在检查租户模型中是否存在同名字段,如果存在则获取结果。

如果我使用控制台查看:

[1] pry(main)> false and tenant.send(:free_courses_enabled?)
=> false

[2] pry(main)> true and tenant.send(:free_courses_enabled?)
NoMethodError: undefined method `free_courses_enabled?' for #<Tenant:0x007f8f7171d6c8>
from /Users/typeoneerror/.rvm/gems/ruby-2.2.4@doki/gems/activemodel-4.2.7.1/lib/active_model/attribute_methods.rb:433:in `method_missing'

如果 Tenant 没有 respond_to? 功能方法,我希望 send 甚至不会被调用,这看起来像是在控制台测试中发生的事情,但我发现我的测试表明,即使 respond_to? returns 为假,也会调用发送但不会引发 NoMethodError

expect(tenant).not_to receive(:free_courses_enabled?)

结果:

expected no Exception, got #<RSpec::Mocks::MockExpectationError: (#<Tenant:0x007fdfd908a2a8>).free_courses_enabled?(no args)
           expected: 0 times with any arguments
           received: 1 time> with backtrace:

有点好笑,当我期望相反的时候:

expect(tenant).to receive(:free_courses_enabled?).and_call_original

我们得到

 NoMethodError:
   undefined method `free_courses_enabled?' for #<Tenant:0x007f8254bfedd8>

我很茫然

在这种情况下,ruby 是否会在调用方法时吞下 NoMethodError 异常,因为表达式的第一部分 returns false?希望得到解释并很高兴听到任何关于重写的建议。

在我看来,它就像是 ActiveModel 的 RSpec 错误。当我不添加期望时,当我 运行 在开发中编写代码时,它会按预期运行。

我想我是在 运行 研究了各种图书馆之后才明白这一点的。 Tenant 没有定义名为 payment_options_features? 的方法,所以当我进入控制台并执行以下操作时:

tenant.respond_to?(:payment_options_features?) # false

但是,我相信我的预期是租户不应在我的 RSpec 测试中收到该方法...

expect(tenant).not_to receive(:payment_options_features?)

实际上在租户对象上创建该函数的模拟,因此当测试运行时:

tenant.respond_to?(:payment_options_features?) # true

因为它是 "mocked"。另一种方法是这样做,因为我们知道 send 是一种应该始终存在的方法。

expect(tenant).not_to receive(:send).with('payment_options_features?')