MiniTest 是什么?不懂的请搜索一下,我就不解释了。
在 MiniTest 之前,用 Ruby 做测试的有两种人,一种人喜欢 Test::Unit 的test_*
风格,另一种人喜欢 Rspec 的describe
风格。他们时不时因为这两种风格的优缺点以及哪一方才能代表真正的 Ruby 测试风格而争执不下。(这有点像《格列佛游记》中,两个小人国因从哪个方向打碎蛋壳而反目成仇)
后来,MiniTest 出现了,改变了这一切。MiniTest 向众人宣讲道,“汝可择 Test::Unit 之道而从之,亦可择 Rspec 而从之”。在 MiniTest 里,你可以像 Test::Unit 那样写测试,也可以像 Rspec 那样写测试。是故,MiniTest 用一种包容的心态解决了纷争,重新给世界带来了和平。
这篇文章,就是讲讲 MiniTest 的正确使用姿势。
使用了 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
的方法,比如前面的setup
和teardown
。
具体的顺序为:
看到这里你多半会觉得,有setup
和teardown
就够了,一大堆别的before/after
还有啥意义啊!文档也提到了,一般用户只需用到setup
和teardown
就够了,其他的 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 还提供了其他进阶功能,比如:
还是用代码做介绍吧。
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
有些时候,测试代码中可能会有这样的调用:它们消耗大把大把时间,占了单元测试的大部分时间;或者会带来不可逆的副作用,比如往远程数据库中添加数据,而你又不能每次执行都清空数据库。 这时候,我们应该怎么办?把调用标记为“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 等等,如果需要,就阅读文档 + 搜索一下吧。