Testing Casual Talks#2に行ってきました。そういえばカバレッジの話がなかったなと思ったので、以前調査した結果を共有したいと思います。
バージョンとか
- ruby (2.1.5)
- simplecov (0.9.2)
カバレッジ(網羅率)とは
よく出てくるカバレッジは下記の3種類でしょう。
-
命令網羅(C0)
テスト内で実行されたメソッドの割合を意味する
-
分岐網羅(C1)
テスト内で実行された分岐の割合を意味する
-
条件網羅(C2)
テスト内で実行された分岐の組み合わせの割合を意味する
def test(x, y)
if (x == 1) || (y == 2)
puts "true"
else
puts "false"
end
end
このようなコードがあったとしましょう。命令網羅の場合、どのような引数であれtest
メソッドをテストしていれば、カバレッジは100%になります。分岐網羅の場合、例えばx = 1, y = 10
とx = 2, y = 10
でtest
メソッドを呼び出していれば、カバレッジは100%になります。条件網羅の場合、if
の前半がtrue
/false
、後半がtrue
/false
という組み合わせがあるため、x = 1, y = 10
とx = 2, y = 2
とx = 1, y = 2
とx = 2, y = 10
の4パターンでtest
メソッドを呼び出していれば、カバレッジは100%になります。
SimpleCovのカバレッジ
SimpleCov gemのカバレッジが上記のうち、どれにあたるかというと、厳密にはどれにも該当していないというのが、正しいと思います。
例として、次のコードのカバレッジを計測してみます。
このコードはgithubにもおいてあるので、結果のhtmlなどは、そちらを参考にしてください。
require "simplecov_sample/version"
module SimplecovSample
class Sample
def initialize(name = "")
@name = name
end
[:test1, :test2, :test3].each do |method_name|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{method_name}() # def test1()
"#{method_name.to_s}" # "test1"
end # end
RUBY_EVAL
end
class_eval do
def test_proc1
"test_proc1"
end
end
[:test4, :test5, :test6].each do |method_name|
define_method(method_name) do
method_name.to_s
end
end
def name(upcase = false)
if upcase
@name.upcase
else
@name
end
end
def name2(upcase = false)
upcase ? @name.upcase : @name
end
def name3(upcase = false)
return @name.upcase if upcase
@name
end
def name4(count = 1)
case count
when 1
@name
when 2
@name * 2
else
""
end
end
def mapped(enum)
enum.map do
1
end
end
def mapped2(enum)
enum.map { 2 }
end
end
end
require 'spec_helper'
describe SimplecovSample do
subject { SimplecovSample::Sample.new("hoge") }
describe "#test1" do
it "" do
expect(subject.test1()).to eq "test1"
end
end
describe "#test_proc1" do
it "" do
expect(subject.test_proc1()).to eq "test_proc1"
end
end
describe "#test4" do
it "" do
expect(subject.test4()).to eq "test4"
end
end
describe "#name" do
it "" do
expect(subject.name).to eq "hoge"
end
end
describe "#name2" do
it "" do
expect(subject.name2(true)).to eq "HOGE"
end
end
describe "#name3" do
it "" do
expect(subject.name3(false)).to eq "hoge"
end
end
describe "#name4" do
it "" do
expect(subject.name4(2)).to eq "hogehoge"
end
end
describe "#mapped" do
it "" do
expect(subject.mapped([])).to eq []
end
end
describe "#mapped2" do
it "" do
expect(subject.mapped2([])).to eq []
end
end
end
命令網羅(C0)として、不十分な点
- 引数が文字列の
class_eval
でメソッドを定義したとき、そのメソッドがテスト対象に含まれない (test1
のケース)
そもそも、テストがカバーすべきメソッドに含まれないということです。
- eachのblock内で
define_method
を用いてメソッドを定義したとき、いずれかのメソッドをテストすれば、他のメソッドもテストしたことになってしまう (test4
のケース)
テストがカバーすべきメソッド数が実際より少なくなってしまいます。
分岐網羅(C1)として、不十分な点
- 三項演算子のうち、片方をtestしていれば、もう一方もテストしたことになってしまう (
name2
のケース)
テストがカバーすべき分岐数が実際より少なくなってしまいます。
- if修飾子を使うと、
false
のときでもテストしたことになってしまう (name3
のケース)
こちらも、テストがカバーすべき分岐数が実際より少なくなってしまいます。
blockのカバレッジについて
each等の引数のblockをワンラインで書くと、eachが一度も回らなくともテストしたことになってしまいます (mapped2
のケース)。
rubyのcoverageライブラリ
SimpleCov gemはrubyの標準ライブラリであるcoverage
を利用しています。このライブラリを追っていくと、rb_iseq_t
つまりRuby の Virtual Machine のコンパイル済みの命令シーケンス
が持っているline番号を使っていることが分かります。このことから、上記の問題はSimpleCovの問題ではなく、coverage
ライブラリの問題だと考えられます。
雑感
カバレッジは参考になる尺度の一つくらいに捉えて、カバレッジにとらわれすぎずにテストを楽しめるのがいいですね。