The Kai Way

Pragmaticly hacking

ZenTest分析

| Comments

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

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