单元测试是如何工作的(从Ruby Koan来学习)

271 阅读2分钟

Ruby Koan是一个学习Ruby语言语法的开源项目,利用测试驱动开发的方式让学习者参与其中,得到及时的学习进度反馈。

它的代码比较简单,本文就通过解读它的关键源码来分析一个简单的单元测试框架是如何识别测试类、测试方法,以及如何显示测试进度的。

Ruby Koan测试结果提示

测试类

新建一个测试类AboutArrays类继承自Neo::Koan类,就可以使用Neo::Koan类中的一系列assert方法了。(这些方法定义在moduleNeo::Assertions中,通过minxin的方式加入Neo::Koan类)

# ./Neo.rb Neo::Assertions
def assert(condition, msg=nil)
  msg ||= "Failed assertion."
  flunk(msg) unless condition
  true
end

def assert_equal(expected, actual, msg=nil)
  msg ||= "Expected #{expected.inspect} to equal #{actual.inspect}"
  assert(expected == actual, msg)
end

当不满足assert条件时,就抛出异常。

# ./Neo.rb Neo::Assertions
FailedAssertionError = Class.new(StandardError)
def flunk(msg)
  raise FailedAssertionError, msg
end

统计测试类和测试方法

Neo::Koan 重写了ruby中Class类的子类继承回调Module类的方法添加回调,当继承了Neo::Koan 类时,子类会被加入Koan的类变量@subclasses中,子类的"test_"开头的方法会被加入子类的类变量@test_methods中。由此就记录下了测试类的数量,以及测试方法的数量,并且都可以由Neo::Koan类访问到。

# ./Neo.rb Neo::Koan
# Class methods for the Neo test suite.
class << self
  def inherited(subclass)
    subclasses << subclass
  end

  def method_added(name)
    testmethods << name if !tests_disabled? && /^test_/ =~ name.to_s
  end
  ...

测试过程的管理

执行ruby path_to_enlightenment.rb时,因为引入了Neo.rb,保证在最后会执行这一句:

END {
  Neo::Koan.command_line(ARGV)
  Neo::ThePath.new.walk # 关注这里
}

Neo::ThePath 负责迭代测试方法,并用Neo::Sencei来管理异常、记录进度。walk时,就是将所有的测试方法迭代执行,koan_index+1就是每个测试类的序号,step_count就是当前测试的是第几个测试方法。

step_count = 0
Neo::Koan.subclasses.each_with_index do |koan,koan_index|
  koan.testmethods.each do |method_name|
    step = koan.new(method_name, koan.to_s, koan_index+1, step_count+=1)
    yield step
  end
end

yield方法最后会调用到Neo::Koan类的meditate方法,该方法会调用无参数的测试方法,并try-catch捕获异常。

# ./Neo.rb Neo::Koan
def meditate
  setup
  begin
    send(name)
  rescue StandardError, Neo::Sensei::FailedAssertionError => ex
    failed(ex)
  ensure
    begin
      teardown
    rescue StandardError, Neo::Sensei::FailedAssertionError => ex
      failed(ex) if passed?
    end
  end
  self
end

每一个meditate方法执行完,Neo:Sensei类都会去检查结果,发现发生异常后,Neo:Sensei类会抛出异常中断Neo:Path的迭代;没有异常发生,就增加@pass_count

打印到屏幕上的结果,显示了pass的测试方法、中断测试的fail的测试方法、异常的msg和关键堆栈、测试进度。

# ./Neo.rb Neo::Sensei
def instruct
  if failed?
    # “ClassName#methodName"
    @observations.each{|c| puts c }
    guide_through_error
    show_progress # passed/total
  else
    end_screen
  end
end

TL;DR

总结下,Ruby Koan测试框架的实现:

  1. Neo::Koan 类提供了assert系列方法,测试类继承后可以使用;
  2. Neo::Koan 类变量记录了所有的子类列表,每个子类的类变量又记录了所有'test_'开头的方法列表;
  3. ruby运行测试类时,通过END语句,在最后开启测试过程;
  4. 测试中,Neo::Path 实例迭代执行所有的测试方法,Neo::Sensei 实例检查每个方法的执行;
  5. 发生异常时,Neo::Sensei 实例中断迭代,打印测试执行的进度,异常的信息。