子不语

RSpec优雅验证之Predicate Matcher

Ruby Test Unit Test RSpec

Ruby 作为动态语言,以其灵活性在测试领域大放异彩。RSpec 作为 Ruby 中使用最广泛的测试工具之一,实在是广大码农们居家旅行测试验证之必备神器。RSpec 提供了强大灵活的验证器(mather),使用这些验证器加上 Ruby 灵活的语法可以写出类似于自然语言的验证,例如:

    result_list.should include(item)

    result.should equal(item)

    person.name.should == 'ddou'

上述验证写法自然看起来赏心悦目,但如下的写法就太不 ruby 了,估计 rubyist 看到了多少会有些反胃:

    person.manager?.should be_true

    detail_view.toggleable?.should == true

    detail_view.has_photo?.should == true

对于上述验证,一个 rubyist 所喜闻乐见的验证写法应该是这样的:

    person.should be_manager

    detail_view.should be_toggleable

    detail_view.should have_photo

这样的写法是不是看起来更自然,更符合人类自然语言的习惯?强大如 RSpec 者自然支持上述语法。RSpec 提供了对 Predicate Matcher 的支持,即可以使用被验证对象自身提供的 predicate 方法作为验证器。 常见的 predicate 方法如 Array.empty?,以及我们上面例子中的 Person.manager?,DetailView.toggleable?,DetailView.has_photo?等。

Predicate Matcher 实现

下面我们就打开 RSpec 源码,看看 Predicate Matcher 是如何实现。

上例中我们并没有定义 be_manager,be_toggleable 方法,RSpec 自然要依赖 Ruby 的强大元编程能力来实现魔法。打开 rspec-expectation 包下 lib/rspec/matchers/method_missing.rb 文件,我们可以看到如下逻辑:

    def method_missing(method, *args, &block)
        return Matchers::BuiltIn::BePredicate.new(method, *args, &block) if method.to_s =~ /^be_/
        return Matchers::BuiltIn::Has.new(method, *args, &block) if method.to_s =~ /^have_/
        super
    end

以上例中 be_manager 为例,此情况下 RSpec 会创建一个 Matchers::BuiltIn::BePredicate 实例,should 会使用该 BePredicate 来验证我们的期望值。下面我们顺藤摸瓜看下 BePredicate 是如何工作的:

    def prefix_and_expected(symbol)
      symbol.to_s =~ /^(be_(an?_)?)(.*)/
      return $1, $3
    end
    def predicate
        "#{@expected}?".to_sym
    end

    def present_tense_predicate
        "#{@expected}s?".to_sym
    end
    def matches?(actual)
        @actual = actual
        begin
            return @result = actual.__send__(predicate, *@args, &@block)
        rescue NameError => predicate_missing_error
            "this needs to be here or rcov will not count this branch even though it's executed in a code example"
        end

        begin
            return @result = actual.__send__(present_tense_predicate, *@args, &@block)
        rescue NameError
            raise predicate_missing_error
        end
    end

HasPredicate 的实现与 BePredicate 类似,此处不再累述。

到此,我们就了解了为什么下面这样的验证能正常工作了:

    person.should be_manager

    detail_view.should be_toggleable

    detail_view.should have_photo

既然了解了 RSpec 的 Predicate Matcher 功能,你还会写出本文初列出的那种 non-ruby 的验证代码吗?