分享 MiniTest 的正确使用姿势

spacewander · 2014年12月08日 · 最后由 douxiance 回复于 2015年09月13日 · 10493 次阅读

MiniTest 是什么?不懂的请搜索一下,我就不解释了。

在 MiniTest 之前,用 Ruby 做测试的有两种人,一种人喜欢 Test::Unit 的test_*风格,另一种人喜欢 Rspec 的describe风格。他们时不时因为这两种风格的优缺点以及哪一方才能代表真正的 Ruby 测试风格而争执不下。(这有点像《格列佛游记》中,两个小人国因从哪个方向打碎蛋壳而反目成仇)

后来,MiniTest 出现了,改变了这一切。MiniTest 向众人宣讲道,“汝可择 Test::Unit 之道而从之,亦可择 Rspec 而从之”。在 MiniTest 里,你可以像 Test::Unit 那样写测试,也可以像 Rspec 那样写测试。是故,MiniTest 用一种包容的心态解决了纷争,重新给世界带来了和平。

这篇文章,就是讲讲 MiniTest 的正确使用姿势。

Hello World

使用了 MiniTest 的测试代码像这样:

require 'minitest/autorun'

# 注意有些资料中,测试类不是继承自MiniTest::Test,
# 那是MiniTest 5之前的做法,MiniTest会通知你改正
class TestMyLife < MiniTest::Test
  # 这个方法会在各个测试之前被调用
  def setup
    @me = People.new
  end

  def test_sleep
     # assert_equal exp, act, msg
     assert_equal   "zzZ", @me.sleep, "I don't sleep well "
  end

  def teardown
  end
end

MiniTest 中可用的断言 (assert_*) 有很多个,具体可以看看文档: http://docs.seattlerb.org/minitest/Minitest/Assertions.html

另外,在每个 test 方法之外,还可以执行被称为lifeCycleHook的方法,比如前面的setupteardown。 具体的顺序为:

  1. before_setup
  2. setup
  3. after_setup
  4. test
  5. before_teardown
  6. teardown
  7. after_teardown

看到这里你多半会觉得,有setupteardown就够了,一大堆别的before/after还有啥意义啊!文档也提到了,一般用户只需用到setupteardown就够了,其他的 hook 是给 MiniTest 拓展用的。

前面说了,MiniTest 兼容并收,即可用 Test::Unit 风格,也可用 Rspec 风格,下面就改用 Rspec 风格写上面的例子:

require 'minitest/spec'
require 'minitest/autorun'

describe "TestMyLife" do
 before do
    @me = People.new
  end

  it "test_sleep" do
     assert_equal   "zzZ", @me.sleep, "I don't sleep well "
  end

  after do
  end
end

看出不同了么?Rspec 风格就是用 describe 替换掉 class,用 it 替换掉测试方法,就是把对象风格的测试用例,变为了 Rake 这样的 DSL 了。注意前面要添加require 'minitest/spec',让 MiniTest 知道现在是 Rspec 风格的测试。

事实上,在 Rspec 风格中,有人会追求用 Expectations 代替 Assertions ,就是写

@me.sleep.must_equal("zzZ", "I don't sleep well ")

而不是

assert_equal   "zzZ", @me.sleep, "I don't sleep well "

不过这个看个人的口味啦。如果你对此有兴趣,看看这个 MiniTest 的 cheet sheet: http://danwin.com/2013/03/ruby-minitest-cheat-sheet/

运行测试

哎呀,啰啰嗦嗦说了一大堆,好像没讲明白如何运行这些测试呢。

其实很简单,直接用 ruby 运行吧。ruby test_file.rb即可。如果要运行多个测试,写一个脚本好了。 不过大家都是用 rake 来运行测试的,事实上 rake 也集成了运行测试的功能。

看看下面的 Rakefile 片段:

Rake::TestTask.new(:test) do |t|
  # libs表示要添加到$LOAD_PATH(就是加载ruby文件的搜索路径)的文件夹
  # 默认是"lib",现在再添加"test"
  t.libs << "test"
  # 要运行的测试文件的特征。匹配以test_开头的所有文件
  t.pattern = 'test/**/test_*.rb' 
  # 不输出测试文件的信息
  t.verbose = false
end

然后运行rake test就能运行测试了。如果你设置task :default => :test,那么仅需rake就能运行默认的任务 - 测试了。 BTW,通过指定rake test TEST=xx,还可以只运行指定文件哦,当一个项目中包含的测试比较多时,只运行相关文件能省下许多时间。

进阶功能

除了前面提到的常用功能,MiniTest 还提供了其他进阶功能,比如:

Benchmark

还是用代码做介绍吧。

require 'minitest/autorun'
require 'minitest/benchmark'

def setup_array(n)
  return Array.new(n){ |i| i }
end

# 需要继承自Benchmark类
class TestBenchmark < MiniTest::Benchmark
  # 所有函数以bench_开头
  def bench_algorithm
    validation = proc { |x, y| x.each_index do |i|
      puts "#{x[i]}\ttime cost: #{y[i]}"
    end
    }
    assert_performance validation do |n|
      ary = setup_array(n)
      100.times do
        ary -ary
      end
    end
  end

  def bench_constant
    # 常数
    assert_performance_constant 0.9 do |n| 
      ary = setup_array(n)
      100.times do
        ary.length
      end
    end
  end

  def bench_logarithmic
    # 查找时间复杂度与其说是log的,不如说是n
    assert_performance_logarithmic 0.9 do |n|
      ary = setup_array(n)
      100.times 
        ary.find 10001
      end
    end
  end

  def bench_linear
    # 线性
    assert_performance_linear 0.9 do |n|
      ary = setup_array(n)
      100.times do
        ary.sort!
      end
    end
  end

  def bench_power
    # n^2
    assert_performance_power 0.9 do |n|
      ary = setup_array(n).shuffle!
      100.times do
        ary - ary
      end
    end
  end

  def bench_exponent
    # 指数
    assert_performance_exponential 0.9 do |n|
      100.times {2 ** n}
    end
  end
end

MiniTest 支持 Benchmark 的测试,你可以测试函数的时间复杂度。这些断言接受两个参数,一个是精确度,另一个是要运行的 block。当然你也可以自定义精确度的计算方式,正如第一个bench_algorithm所示。(虽然在那里我只是把输入和运行时间打印出来。)另外你也可以自定义参数的范围,默认条件下是从 1 到 10000,按 10 倍增长。

用 Rspec 风格写 Benchmark 也是可能的,具体看文档。 http://docs.seattlerb.org/minitest/Minitest/BenchSpec.html

Mock

有些时候,测试代码中可能会有这样的调用:它们消耗大把大把时间,占了单元测试的大部分时间;或者会带来不可逆的副作用,比如往远程数据库中添加数据,而你又不能每次执行都清空数据库。 这时候,我们应该怎么办?把调用标记为“FIXME”,然后交由维护的程序员头疼去? 在软件测试中,我们可以使用 Mock 来解决这个问题。我们不需要真的做这个调用,而是返回一个预期的值,交由要测试的方法进行处理。这样,我们无需进行完整的调用,就可以以合理的输入来测试方法。

MiniTest 也集成了 Mock 的功能。一如既往,我还是直接 show you the code:

require 'minitest/autorun'

class TestMock < MiniTest::Test
  def setup
    @badman = MiniTest::Mock.new
    # expect :method_name, retval, args=[]
    #@badman.expect(:destroy_my_computer, true, [String])
    @badman.expect(:destroy_my_computer, true)
    @goodman = MiniTest::Mock.new
    @goodman.expect(:destroy_my_computer, false)
  end

  def test_mock
    assert_equal true, @badman.destroy_my_computer
    assert_equal false, @goodman.destroy_my_computer
    #assert_equal true, @badman.destroy_my_computer('brutally')
  end

end

所有 Mock 的实例都有一个expect方法,接受:method_name, retval, args 这三个参数。其中 args 是输入参数数组,表示允许的输入范围。而 retval 是对应的返回值。

MiniTest 还有其他一些功能我没有介绍,比如允许第三方插件自定义 test reporter 等等,如果需要,就阅读文档 + 搜索一下吧。

本来只有两个测试框架的,为了统一,结果出现了第三个

这么良心的贴没人吗?

我倒认为minitest帮了倒忙。见仁见智吧。

还是欠缺了很多功能 不支持 nested context 和 before/after all, 缺乏 json 等输出格式不方便其他软件集成

Minitest 现在是 Ruby 自带的,Test::Unit 是用它重新实现的,不太存在多了一个框架的问题吧。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号