minitest的assertions和expectations分析
跟踪一下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它