Minitest是Ruby中最简单的测试框架,和RSpec相比,语法简单,容易上手。上周使用的时候被它能够自动执行测试类中测试方法的特性所吸引,最近又在学习元编程,感觉可以学习下它的实现思路,便一头扎入源码的阅读中。在这里记录下源码分析的过程:

获取当前测试类

从测试实例开始

1
2
3
4
5
6
7
8
9
10
11
12
13
# cat_test.rb
require "minitest/autorun"

class TestCat < Minitest::Test
def setup
@cat = Cat.new
end

def test_meow
# 相等
assert_equal "Meow~!", @cat.meow
end
end

首先,Minitest要执行到声明的测试类,在Minitest中要获取到声明的测试类才能执行其中的方法,那么在Minitest是如何获取到声明的类呢?

在Minitest中的minitest.rb

1
2
3
4
5
6
7
# minitest.rb 
class Runnable # re-open
def self.inherited klass # :nodoc:
self.runnables << klass
super
end
end

在minitest.rb中的Runnable类中有个inherited方法,这个方法是当前类的子类或者当前类创建时的回调,在test.rb中的Test类继承自Runnable这个类,而在测试用例的使用中测试类又继承自Test类,所以在每次写测试用例的时候在Test中就能获取到声明的子类。

1
2
3
class Test < Runnable
...
end

在测试类声明之后示例化

通过inherited方法,minitest可以获取到我们声明的测试类了,但我们的声明在require 'mintiest/autorun'之后,在测试文件声明测试类之后就结束运行了。那minitest是如何把声明的测试类实例化并调用其方法了呢?在autorun.rb中的Minitest.autorun中,有几个at_exit方法,它被包含在Kernel模块中。它可以把传递给它的块转换为Proc对象,并在程序退出的时候调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# minitest.rb  
def self.autorun
at_exit {
next if $! and not ($!.kind_of? SystemExit and $!.success?)

exit_code = nil

at_exit {
@@after_run.reverse_each(&:call)
exit exit_code || false
}

exit_code = Minitest.run ARGV
} unless @@installed_at_exit
@@installed_at_exit = true
end

找到测试类声明的方法

找到类中的声明方法在

1
2
3
4
5
6
7
8
# minitest.rb
class Runnable
...
def self.methods_matching re
public_instance_methods(true).grep(re).map(&:to_s)
end
...
end

查找方法

1
2
3
4
5
6
7
8
9
# test.rb
class Test
...
def self.runnable_methods
methods = methods_matching(/^test_/)
...
end
...
end

在获取可运行的方法使用了Ruby 类的public_instance_methods方法,这个方法返回在mod中定义的公共实例方法的列表。如果可选参数为false,则不包括任何祖先的方法。可以看到这里只匹配测试类中的以test_开头的方法。

运行测试方法

1
2
3
4
# minitest.rb
filtered_methods.each do |method_name|
run_one_method self, method_name, reporter
end

在做了一些额外的筛选之后,运行run_one_method

这最终调用了Minitest : : Test上的运行实例方法:

1
2
3
4
capture_exceptions do
before_setup; setup; after_setup
self.send self.name
end

总结

在Minitest中使用了大量的Ruby元编程特性,这些特性如果熟练灵活运用可以创造很多新奇的东西。