Skip to main content
The Kai Way

ZenTest分析

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

整个程序的主要分为三个部分,第一个是导入文件并收集文件中的类的信息,第二部分是收集前一部分收集到的类的方法的信息进行处理,最后是生成测试类的代码。