跟踪一下must_equal是干啥的

require 'minitest/spec'
require 'minitest/autorun'
require 'trace_tree'

describe Numeric do
  it 'is ok' do
    binding.trace_tree do
      1.must_equal 1
    end
  end
end


调用栈如下

Numeric#block (3 levels) in 
it.rb:7 └─Fixnum#must_equal $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:10   ├─Minitest::Spec.current $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:97   └─Minitest::Expectation#must_equal $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:16     └─Numeric#assert_equal $GemPath0/gems/minitest-5.10.1/lib/minitest/assertions.rb:172       ├─Numeric#message $GemPath0/gems/minitest-5.10.1/lib/minitest/assertions.rb:512       └─Numeric#assert $GemPath0/gems/minitest-5.10.1/lib/minitest/assertions.rb:134


如minitest-5.10.1/lib/minitest/spec.rb:10所示,数字1的must_equal方法是通过Module#infect_an_assertion动态定义的,它所做的就是调用Minitest::Expectation#must_equal(也是动态定义)

require "minitest/test"

class Module # :nodoc:
  def infect_an_assertion meth, new_name, dont_flip = false # :nodoc:
    block = dont_flip == :block
    dont_flip = false if block

    # warn "%-22p -> %p %p" % [meth, new_name, dont_flip]
    self.class_eval <<-EOM, __FILE__, __LINE__ + 1
      def #{new_name} *args
        Minitest::Expectation.new(self, Minitest::Spec.current).#{new_name}(*args)
      end
    EOM

    # 这里ctx就是Minitest::Spec.current,即Minitest::Spec < Test,继承了assert_xxx方法
    # target就是上例的数字1,来自于self
    # 1.must_equal 1相当于在TestCase中调用assert_equal 1, 1

    Minitest::Expectation.class_eval <<-EOM, __FILE__, __LINE__ + 1
      def #{new_name} *args
        case
        when #{!!dont_flip} then
          ctx.#{meth}(target, *args)
        when #{block} && Proc === target then
          ctx.#{meth}(*args, &target)
        else
          ctx.#{meth}(args.first, target, *args[1..-1])
        end
      end
    EOM
  end
end

Minitest::Expectation = Struct.new :target, :ctx # :nodoc:


为什么数字1会有must_equal呢,答案在Minitest::Expectations。当然它是module,所以获取到了Module#infect_an_assertion,并用此方法来为自己增加了must_equal、must_be_empty等实例方法(minitest-5.10.1/lib/minitest/spec.rb:10)

然后Object.include Minitest::Expectations,因而所有Object都有了must_equal、must_be_empty等实例方法

(注意Minitest::Expectations不同Minitest::Expectation)

require "minitest/expectations"

class Object # :nodoc:
  include Minitest::Expectations unless ENV["MT_NO_EXPECTATIONS"]
end


所有infect_an_assertion如下,有点像在做alias_method

第三个参数有四种可能::unary、:reverse、:block、nil,前两个走第一个when,block走第二个,nil走else

:reverse的意思是原本assert_xxx的语义是反的,例如assert_respond_to String.new, :to_s

:unary的意思是原本assert_xxx不接收expect对象做参数,只接收actual

刚好这两种assert_xxx都符合ctx.#{meth}(target, *args)的形式

$ lib git:(master) grep -v '#' minitest/expectations.rb | grep -v '^$'
module Minitest::Expectations
  infect_an_assertion :assert_empty, :must_be_empty, :unary
  infect_an_assertion :assert_equal, :must_equal
  infect_an_assertion :assert_in_delta, :must_be_close_to
  infect_an_assertion :assert_in_epsilon, :must_be_within_epsilon
  infect_an_assertion :assert_includes, :must_include, :reverse
  infect_an_assertion :assert_instance_of, :must_be_instance_of
  infect_an_assertion :assert_kind_of, :must_be_kind_of
  infect_an_assertion :assert_match, :must_match
  infect_an_assertion :assert_nil, :must_be_nil, :unary
  infect_an_assertion :assert_operator, :must_be, :reverse
  infect_an_assertion :assert_output, :must_output, :block
  infect_an_assertion :assert_raises, :must_raise, :block
  infect_an_assertion :assert_respond_to, :must_respond_to, :reverse
  infect_an_assertion :assert_same, :must_be_same_as
  infect_an_assertion :assert_silent, :must_be_silent, :block
  infect_an_assertion :assert_throws, :must_throw, :block
  infect_an_assertion :refute_empty, :wont_be_empty, :unary
  infect_an_assertion :refute_equal, :wont_equal
  infect_an_assertion :refute_in_delta, :wont_be_close_to
  infect_an_assertion :refute_in_epsilon, :wont_be_within_epsilon
  infect_an_assertion :refute_includes, :wont_include, :reverse
  infect_an_assertion :refute_instance_of, :wont_be_instance_of
  infect_an_assertion :refute_kind_of, :wont_be_kind_of
  infect_an_assertion :refute_match, :wont_match
  infect_an_assertion :refute_nil, :wont_be_nil, :unary
  infect_an_assertion :refute_operator, :wont_be, :reverse
  infect_an_assertion :refute_respond_to, :wont_respond_to, :reverse
  infect_an_assertion :refute_same, :wont_be_same_as
end


各种assertions就定义在Minitest::Assertions中,然后Minitest::Test会include它