设有如下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