ZenTest是Ruby的一个确保测试覆盖率和促进TDD实行(Ensures test coverage and accelerates TDD)的代码生成框架,并带有其它的实用工具(autotest等等)。官方主页:http://www.zenspider.com/ZSS/Products/ZenTest/
这次我选了ZenTest的1.0版来研究,代码文件大小只有3.7K,144行。
下面是代码:
#!/usr/local/bin/ruby -w -I.
VERSION = '1.0.0'
puts "# Created with ZenTest v. #{VERSION}"
$AUTOTESTER = true
# 重写at_exit方法,让该方法不做任何事情
module Kernel
alias :old_at_exit :at_exit
def at_exit()
# nothing to do...
end
end
require 'test/unit'
require 'test/unit/ui/console/testrunner'
# 产生文件的副本
files = ARGV.clone
# 保存测试类的hash( klass -> klassmethods)
test_klasses = {}
# 保存待测试类的hash( klass -> klassmethods)
klasses = {}
# 保存待测试类的方法的hash( klass -> klassmethods)
all_methods = {} # fallback
# 迭代处理参数指定的每个文件
ARGV.each do |file|
# 导入文件,失败则抛出异常,并输出错误信息
begin
require "#{file}"
rescue LoadError => err
puts "Couldn't load #{file}: #{err}"
next
end
# 迭代处理文件的每一行
IO.foreach(file) do |line|
# 如果该行中包含类定义(class Xxx)则处理
if line =~ /^\s*class\s+(\S+)/ then
# 保存类名
klassname = $1
# 将类名转换为Symbol后再得到该类的类型值
klass = Module.const_get(klassname.intern)
# 如果该类为单元测试类
target = klassname =~ /^Test/ ? test_klasses : klasses
# record public instance methods JUST in this class
# 保存类的公用实例方法到一个methond -> boolean的Hash中
# 并将该Hash保存到以该类为键值的target hash中
public_methods = klass.public_instance_methods
klassmethods = {}
public_methods.each do |meth|
klassmethods[meth] = true
end
target[klassname] = klassmethods
# record ALL instance methods including superclasses (minus Object)
# 将该类中由Object类继承的实例方法除去后保存
the_methods = klass.instance_methods(true) - Object.instance_methods(true)
# 生成同上个代码块类似的hash
klassmethods = {}
the_methods.each do |meth|
klassmethods[meth] = true
end
all_methods[klassname] = klassmethods
end
end
end
# 上面的迭代代码块运行完成后将记录下文件中所有类和其公共的实例方法
print "# "
p all_methods
missing_methods = {} # key = klassname, val = array of methods
# 在待测试类上进行迭代(以待测试类的名字)
klasses.each_key do |klassname|
# 生成测试类类名
testklassname = "Test#{klassname}"
if test_klasses[testklassname] then
methods = klasses[klassname]
testmethods = test_klasses[testklassname]
# check that each method has a test method
# 检查每个方法是否有一个测试方法
klasses[klassname].each_key do | methodname |
testmethodname = "test_#{methodname}".gsub(/\[\]=/, "index_equals").gsub(/\[\]/, "index_accessor")
unless testmethods[testmethodname] then
puts "# ERROR method #{testklassname}\##{testmethodname} does not exist (1)" if $VERBOSE
missing_methods[testklassname] ||= []
missing_methods[testklassname].push(testmethodname)
end
end
# check that each test method has a method
testmethods.each_key do | testmethodname |
if testmethodname =~ /^test_(.*)/ then
methodname = $1.gsub(/index_equals/, "[]=").gsub(/index_accessor/, "[]")
# try the current name
orig_name = methodname.dup
found = false
until methodname == "" or methods[methodname] or all_methods[klassname][methodname] do
# try the name minus an option (ie mut_opt1 -> mut)
if methodname.sub!(/_[^_]+$/, '') then
if methods[methodname] or all_methods[klassname][methodname] then
found = true
end
else
break # no more substitutions will take place
end
end
unless found or methods[methodname] or methodname == "initialize" then
puts "# ERROR method #{klassname}\##{orig_name} does not exist (2)" if $VERBOSE
missing_methods[klassname] ||= []
missing_methods[klassname].push(orig_name)
end
else
unless testmethodname =~ /^util_/ then
puts "# WARNING Skipping #{testklassname}\##{testmethodname}"
end
end
end
else
puts "# ERROR test class #{testklassname} does not exist" if $VERBOSE
missing_methods[testklassname] ||= []
klasses[klassname].keys.each do |meth|
missing_methods[testklassname].push("test_#{meth}")
end
end
end
# 真正地创建测试类,为没有测试的类添加方法。
missing_methods.keys.sort.each do |klass|
# 判断是否为测试类
testklass = klass =~ /^Test/
puts "class #{klass}" + (testklass ? " < Test::Unit::TestCase" : '')
methods = missing_methods[klass] | []
m = []
methods.sort.each do |method|
# 为测试类生成代码
if testklass then
s = " def #{method}\n assert(false, 'Need to write #{method} tests')\n end"
else
s = " def #{method}\n # TO" + "DO: write some code\n end"
end
m.push(s)
end
puts m.join("\n\n")
puts "end"
puts ""
end
整个程序的主要分为三个部分,第一个是导入文件并收集文件中的类的信息,第二部分是收集前一部分收集到的类的方法的信息进行处理,最后是生成测试类的代码。