Minitest::Spec浅析
设有如下minitest spec
require 'minitest/spec'
require 'minitest/autorun'
require 'trace_tree'
binding.trace_tree do
describe Numeric do
it 'is ok' do
1.must_equal 1
end
end
end
输出调用栈如下
Object#block in
it.rb:5 └─Object#describe $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:71 ├─Minitest::Spec.describe_stack $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:159 ├─Minitest::Spec.spec_type $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:149 │ └─Minitest::Spec.block in spec_type $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:150 ├─Minitest::Spec.create $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:259 │ ├─Minitest::Spec.inherited $GemPath0/gems/minitest-5.10.1/lib/minitest.rb:268 │ │ └─Minitest::Spec.runnables $GemPath0/gems/minitest-5.10.1/lib/minitest.rb:365 │ ├─Numeric.block in create $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:260 │ │ └─Numeric.nuke_test_methods! $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:167 │ └─Minitest::Spec.children $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:163 └─Numeric.block (2 levels) in
it.rb:6 └─Numeric.it $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:212 └─Numeric.children $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:163
describe方法分析如下
module Kernel
def describe desc, *additional_desc, &block # :doc:
# 从当前线程取出一个“含有多个describe”的栈
# 原本以为“含有多个”是指并列定义多个describe
# 但看到最后的push和pop,才发现说的是嵌套定义多个describe
stack = Minitest::Spec.describe_stack
name = [stack.last, desc, *additional_desc].compact.join("::")
# 1.如果当前describe是嵌套在另一describe中的,则stack.last有值,为上一级的describe
# 2.因为顶级的describe还可以写在class MySpec < Minitest::Spec中,所以这种情况直接返回当前类
# 3.如果顶级的describe是写在main object或其它类中,则调Minitest::Spec.spec_type返回Minitest::Spec
sclas = stack.last || if Class === self && kind_of?(Minitest::Spec::DSL) then
self
else
Minitest::Spec.spec_type desc, *additional_desc
end
cls = sclas.create name, desc
# 入栈,执行block
# 以使下级describe能基于本类来作子类化(block中有describe),或定义feature(使用it)
# 出栈,以便同级的describe进来
stack.push cls
cls.class_eval(&block)
stack.pop
cls
end
end
spec_type方法如下,似乎无论如何都会返回Minitest::Spec。虽然有register_spec_type可增加Spec类,但似乎没人提
TYPES = [[//, Minitest::Spec]]
def spec_type desc, *additional
TYPES.find { |matcher, _klass|
if matcher.respond_to? :call then
matcher.call desc, *additional
else
matcher === desc.to_s
end
}.last
end
create如下,虽然感觉更应直接放在Minitest::Spec中
另外,从刚才调用栈可看出,新建子类是为了配合minitest“在included中将新类加入到runnables,以便at_exit时调用test_xxx”的规则
class Minitest::Spec
module DSL
def create name, desc # :nodoc:
cls = Class.new(self) do
@name = name
@desc = desc
# 删除public_instance_methods是为了避免重复运行test_xxx
# 一方面,你可以在class MySpec < Minitest::Spec中,混合使用test_xxx和describe
# 因Minitest::Spec < Test < Runnable,任何Runnable子类都会被included加入到runnables以便at_exit时调用test_xxx
# 而describe会如上Class.new(self)产生子类,加入到runnables
# 另一方面,每次嵌套describe,都会基于上级describe来产生子类
# 如果上级describe已经用it来生成了test_xxx,那这些test_xxx又会被继承
# 所以必须在生成子类之后,屏蔽子类获得的test_xxx,以免再被at_exit调用
nuke_test_methods!
end
children << cls
cls
end
end
extend DSL
end
it所做的就是生成test_xxx方法,并找出children中非明确定义it或test_xxx的哪些子类,屏蔽它们对本次it所生成的test_xxx的继承
注意这里屏蔽方法的做法和nuke_test_methods!一样,都是undef_method,它甚至阻止对父类继承方法的调用,有别于remove_method
同时,还发现方法名可有空格,这是def无法做到的。当然调用时也没法直接调用,必须send
def it desc = "anonymous", &block
block ||= proc { skip "(no tests defined)" }
@specs ||= 0
@specs += 1
name = "test_%04d_%s" % [ @specs, desc ]
undef_klasses = self.children.reject { |c| c.public_method_defined? name }
define_method name, &block
undef_klasses.each do |undef_klass|
undef_klass.send :undef_method, name
end
name
end
再测试一下嵌套describe
require 'minitest/spec'
require 'minitest/autorun'
require 'trace_tree'
binding.trace_tree do
describe Numeric do
it 'is ok' do
1.must_equal 1
end
describe Fixnum do
it 'is fine' do
(1 + 1).must_equal 2
end
end
end
end
调用栈如下,符合预期
Object#block in
it.rb:5 └─Object#describe $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:71 ├─Minitest::Spec.describe_stack $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:159 ├─Minitest::Spec.spec_type $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:149 │ └─Minitest::Spec.block in spec_type $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:150 ├─Minitest::Spec.create $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:259 │ ├─Minitest::Spec.inherited $GemPath0/gems/minitest-5.10.1/lib/minitest.rb:268 │ │ └─Minitest::Spec.runnables $GemPath0/gems/minitest-5.10.1/lib/minitest.rb:365 │ ├─Numeric.block in create $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:260 │ │ └─Numeric.nuke_test_methods! $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:167 │ └─Minitest::Spec.children $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:163 └─Numeric.block (2 levels) in
it.rb:6 ├─Numeric.it $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:212 │ └─Numeric.children $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:163 └─Numeric.describe $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:71 ├─Minitest::Spec.describe_stack $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:159 ├─Numeric.to_s $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:276 │ └─Numeric.name $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:272 ├─Numeric.create $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:259 │ ├─Numeric.inherited $GemPath0/gems/minitest-5.10.1/lib/minitest.rb:268 │ │ └─Numeric.runnables $GemPath0/gems/minitest-5.10.1/lib/minitest.rb:365 │ ├─Numeric::Fixnum.block in create $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:260 │ │ └─Numeric::Fixnum.nuke_test_methods! $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:167 │ │ └─Numeric::Fixnum.block in nuke_test_methods! $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:168 │ └─Numeric.children $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:163 └─Numeric::Fixnum.block (3 levels) in
it.rb:11 └─Numeric::Fixnum.it $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:212 └─Numeric::Fixnum.children $GemPath0/gems/minitest-5.10.1/lib/minitest/spec.rb:163
下层的describe的类名,Numeric::Fixnum,是这样得出的
name = [stack.last, desc, *additional_desc].compact.join("::")
然后所有describe类写死调@name或返回父describe的name
def create name, desc # :nodoc:
cls = Class.new(self) do
@name = name
@desc = desc
nuke_test_methods!
end
children << cls
cls
end
def name # :nodoc:
defined?(@name) ? @name : super
end