分享 RSpec 如何实现动态定义 be_matcher

treehacker · April 04, 2019 · Last by laocainiao replied at April 10, 2019 · 1971 hits

第一次发文 不足望指正😃

在官方文档中https://www.rubydoc.info/gems/rspec-rails/1.3.4/Spec/Rails/Matchers,提到了

response.should be_success #passes if response.success?
response.should be_redirect #passes if response.redirect?

但并没有提着个方法是从哪里来的, 接下来就详细讲解下 假设单元测试中一个断言为

expect(my_api_get_user.request).to be_success

首先会动态定义 be_success 方法,这个是通过/.rvm/gems/ruby-2.3.1/gems/rspec-expectations-3.7.0/lib/rspec/matchers.rb:953 的 BE_PREDICATE_REGEX 来定义的

def method_missing(method, *args, &block)
      case method.to_s
      when BE_PREDICATE_REGEX
        BuiltIn::BePredicate.new(method, *args, &block)
      when HAS_REGEX
      #....

然后紧接着就会运行这个刚刚动态定义完的 be_success 紧接着就触发下面的追溯链 (TraceBack stack):

matches? [be.rb:193] (RSpec::Matchers::BuiltIn::BePredicate)
handle_matcher [handler.rb:50] (RSpec::Expectations::PositiveExpectationHandler)
with_matcher [handler.rb:27] (RSpec::Expectations::ExpectationHelper)
handle_matcher [handler.rb:48] (RSpec::Expectations::PositiveExpectationHandler)
to [expectation_target.rb:65] (RSpec::Expectations::ExpectationTarget::InstanceMethods)
my_api_get_user_spec.rb:15
//....

最顶端的 RSpec::Matchers::BuiltIn::BePredicate#matches?重写了 matches?方法,重写 matches?方法是自定义 Matcher 的手段:

#actual在这里就是{Symbol}success?
def matches?(actual, &block)
  @actual  = actual #actual is the instnce of my_api_get_user
  @block ||= block #block can be nil
  predicate_accessible? && predicate_matches?
end

predicate_accessible?用于确认 method sucess 是否真的存在于 my_api_get_user predicate_matches?的实现方式

#/.rvm/gems/ruby-2.3.1/gems/rspec-expectations-3.7.0/lib/rspec/matchers/built_in/be.rb:190
def predicate_matches?
    method_name = actual.respond_to?(predicate) ? predicate : present_tense_predicate
    @predicate_matches = actual.__send__(method_name, *@args, &@block)
 end

可以看到 actual.send(method_name, *@args, &@block) 就是相当于,send是 ruby 中元编程动态定义方法的核心

my_api_get_user.success?(*@args,&@block)

如果返回的是 true 的话,最终会在这里做判断 (可以在之前的回溯链中找到 handle_matcher [handler.rb:48]):

#/.rvm/gems/ruby-2.3.1/gems/rspec-expectations-3.7.0/lib/rspec/expectations/handler.rb:47
def self.handle_matcher(actual, initial_matcher, message=nil, &block)
  ExpectationHelper.with_matcher(self, initial_matcher, message) do |matcher|
    return ::RSpec::Matchers::BuiltIn::PositiveOperatorMatcher.new(actual) unless initial_matcher
    matcher.matches?(actual, &block) || ExpectationHelper.handle_failure(matcher, message, :failure_message)
  end
end
matcher.matches?(actual, &block) || ExpectationHelper.handle_failure(matcher, message, :failure_message)

如果 matcher.matches?(actual, &block) 返回为 true,则断言为 true,否则就调用 ExpectationHelper.handle_failure 来出来错误断言

看不懂,你到底是在用 rspec 1 还是 rspec 3, 还有你的问题是什么?

Reply to yakjuly

rspec 3 吧 没问题啊

这是 Rspec 引进不必要的复杂的一个体现,在 Minitest 里面定义自己的断言只需要定义 Ruby 方法:

def assert_what_you_want(*args)
  # do something
  assert(except, actual, message)
end

用 MiniTest,需要自定义断言像上面 @Rei 那样定义一个函数就好了,非常简单

MiniTest 看似提供的内容很少,但实际上往往我们在项目里面根据自己的需要不断累积出自己的 assert_xxx 方法以后会变得非常好使。这在 Rails 源代码的测试里面大量在用。

此外就算使用 RSpec 楼主你想做的事情也可以试试用定义 assert 方法来解决,这样会简单很多。我们往往验证的需求不就是判断 a 与 b 两个值么?

我也是喜欢比较纯粹的东西。MiniTest 就够了。

You need to Sign in before reply, if you don't have an account, please Sign up first.