SimpleCovのカバレッジ(網羅率)


[Testing Casual Talks#2](https://atnd.org/events/66159)に行ってきました。そういえばカバレッジの話がなかったなと思ったので、以前調査した結果を共有したいと思います。

## バージョンとか

* ruby (2.1.5)
* simplecov (0.9.2)

## カバレッジ(網羅率)とは

よく出てくるカバレッジは下記の３種類でしょう。

* 命令網羅（C0）

  テスト内で実行されたメソッドの割合を意味する

* 分岐網羅（C1）

  テスト内で実行された分岐の割合を意味する

* 条件網羅（C2）

  テスト内で実行された分岐の組み合わせの割合を意味する

```ruby
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`の４パターンで`test`メソッドを呼び出していれば、カバレッジは100%になります。

## SimpleCovのカバレッジ

SimpleCov gemのカバレッジが上記のうち、どれにあたるかというと、厳密にはどれにも該当していないというのが、正しいと思います。

例として、次のコードのカバレッジを計測してみます。
このコードは[github](https://github.com/yui-knk/simplecov_sample)にもおいてあるので、結果のhtmlなどは、そちらを参考にしてください。

```ruby:simplecov_sample.rb
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
```

```ruby:simplecov_sample_spec.rb
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）として、不十分な点

1. 引数が文字列の`class_eval`でメソッドを定義したとき、そのメソッドがテスト対象に含まれない (`test1`のケース)

  そもそも、テストがカバーすべきメソッドに含まれないということです。

1. eachのblock内で`define_method`を用いてメソッドを定義したとき、いずれかのメソッドをテストすれば、他のメソッドもテストしたことになってしまう (`test4`のケース)

  テストがカバーすべきメソッド数が実際より少なくなってしまいます。

### 分岐網羅（C1）として、不十分な点

1. 三項演算子のうち、片方をtestしていれば、もう一方もテストしたことになってしまう (`name2`のケース)

  テストがカバーすべき分岐数が実際より少なくなってしまいます。

1. if修飾子を使うと、`false`のときでもテストしたことになってしまう (`name3`のケース)

  こちらも、テストがカバーすべき分岐数が実際より少なくなってしまいます。

### blockのカバレッジについて

each等の引数のblockをワンラインで書くと、eachが一度も回らなくともテストしたことになってしまいます (`mapped2`のケース)。

## rubyのcoverageライブラリ

SimpleCov gemはrubyの標準ライブラリである`coverage`を利用しています。このライブラリを追っていくと、`rb_iseq_t`つまり`Ruby の Virtual Machine のコンパイル済みの命令シーケンス`が持っているline番号を使っていることが分かります。このことから、上記の問題はSimpleCovの問題ではなく、`coverage`ライブラリの問題だと考えられます。

## 雑感

カバレッジは参考になる尺度の一つくらいに捉えて、カバレッジにとらわれすぎずにテストを楽しめるのがいいですね。