Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

SimpleCovのテストカバレッジ計測について

この記事は Money Forward Engineering 2 Advent Calendar 2022 の15日目の記事です。 昨日はsh-ogawaさんの ローカル開発環境で、Kafka Producerのトランザクションを単一ブローカーで動くようにする でした。

こんにちは、マネーフォワードでバックエンドエンジニアとして働いているHiroVodkaです。

皆さんはSimpleCovというGemを知っていますか?? https://github.com/simplecov-ruby/simplecov

Rubyで開発されているサービスのテストカバレッジを測るために使われることが多いGemだと思います。 今回はSimpleCovがどのような基準でカバレッジを計測しているのかを、実際に手元で動かしながら調査してみました。

※ この記事ではテストカバレッジについての詳しい解説(ラインカバレッジ、ブランチカバレッジ等)はせずに、SimpleCovに関しての内容を記載しています🙇

ラインカバレッジの計測

SimpleCovではラインカバレッジ、ブランチカバレッジを計測することができるため、まずはラインカバレッジを計測してみます。

早速、簡単な処理を記述したクラスを作成して検証してみます。 PlusOneクラスはインスタンスを作成した後、callメソッドを呼ぶと@numの値が1増えるようなクラスです。

class PlusOne
  attr_reader :num
  def initialize(attr)
    @num = attr
  end

  def call
    @num += 1
  end
end

まずはこのメソッドに対応したテストを書きます。 普段の開発でRSpecを使用しているため、今回もRSpecを使って検証していこうと思います。

require 'simplecov'
SimpleCov.start

RSpec.describe PlusOne do

  describe "callメソッドのテスト" do
    it "引数に1が渡されたplus_oneオブジェクトをcallすると@numの値が1増えること" do
      obj = PlusOne.new(1)
      obj.call
  
      expect(obj.num).to eq 2
    end
  end
end

そしてspecを実行した後のSimpleCovの結果がこちらです。

カバレッジが100パーセントになっています。

テストが書かれていないメソッドが存在する状態

PlusOneクラスに新たなメソッドを追加します。

class PlusOne
  attr_reader :num
  def initialize(attr)
    @num = attr
  end

  def call
    @num += 1
  end
+
+  def no_call_method
+    puts '呼ばれないです'
+  end
 end

そして、追加されたメソッドに対するテストコードは書かれていない状態でテストしてみます。

このように11行目のno_call_methodがRSpecでテストされていないため、赤くなっています。 また、カバレッジも87.5パーセントになっています。

GUI上でテストが書かれていない該当の箇所を分かりやすく見ることができるのはとても便利ですね。

ifを使った条件分岐が存在する場合

ここまでの検証で、行自体がテスト中に実行されたかどうかでカバレッジを測ってることが分かりました。

なので、if文を使った条件分岐が存在するとカバレッジがどうなるのかを検証してみましょう。

class PlusOne
  attr_reader :num
  def initialize(attr)
    @num = attr
  end

  def call
    # initializeされた時に引数が0以外なら1を足す。0なら値を変えない
    if @num != 0
      @num += 1
    end
  end
end

callメソッド内に if @num != 0というif文を追加しました。 テストコードは今までと同じです。

この状態でspecを実行すると、カバレッジは100パーセントになります。

ifの条件分岐は、callメソッドを実行すると処理されるので、『処理が行われた行』に対してカバレッジを測っているっぽいですね🤔

また早期returnを追加した場合も同様の結果になります。

class PlusOne
  attr_reader :num
  def initialize(attr)
    @num = attr
  end

  def call
    # initializeした時の引数が0ならメソッドを抜ける(1を足さない)
    return if @num == 0

    @num += 1
  end
end

やっぱりカバレッジが100パーセントになりました。

もしかすると、こんな感じで書かれているif文なら検知してくれてそうな気がするので試してみます。

class PlusOne
  attr_reader :num
  def initialize(attr)
    @num = attr
  end

  def call
    # initializeの引数が0以外なら1を足す、0ならraise(エラー)
    if @num != 0
      @num += 1
    else
      raise
    end
  end
end

この状態でテストを変えずに実行してみます。

else句の処理はテスト中に実行されないのでカバレッジは87.5パーセントになっていますね。

つまり、このコードのままカバレッジを100パーセントにするには、else句を通るようなテストを書く必要があります。

require 'simplecov'
SimpleCov.start

RSpec.describe PlusOne do

  describe "callメソッドのテスト" do
    it "引数に1が渡されたplus_oneオブジェクトをcallすると@numの値が1増えること" do
      obj = PlusOne.new(1)
      obj.call
  
      expect(obj.num).to eq 2
    end

    # 新しく追加したテスト↓
    it "引数に0が渡されたplus_oneオブジェクトをcallするとエラーが発生すること" do
      obj = PlusOne.new(0)

      expect { obj.call }.to raise_error(RuntimeError)
    end
  end
end

これでカバレッジが100パーセントになりました。

三項演算子を使った条件分岐

ifを使用した条件分岐がある場合の挙動は分かったので、次は三項演算子で試してみます。

class PlusOne
  attr_reader :num
  def initialize(attr)
    @num = attr
  end

  def call
   # initializeした時の引数が0以外なら1を足す、0なら例外(エラー)を発生させる
    @num != 0 ? (@num += 1) : raise
  end
end

テストは三項演算子のelse句を通らないようなテストにしておきます。

require 'simplecov'
SimpleCov.start

RSpec.describe PlusOne do

  describe "callメソッドのテスト" do
    it "引数に1が渡されたplus_oneオブジェクトをcallすると@numの値が1増えること" do
      obj = PlusOne.new(1)
      obj.call
  
      expect(obj.num).to eq 2
    end
  end
end

この状態で実行すると、カバレッジは100パーセントになります。 つまり、三項演算子は一行なので条件文の値が真になる場合だけ通ってもラインカバレッジとして計測されるみたいですね。

というわけで普通にsimplecovを利用すると

  • テストで実行されていない行が分かる
    • 全く実行されていないメソッドは検知できる
  • 条件分岐等のカバレッジは正確に計測できない
    • if, elseが複数行に渡っている場合はテストしてないことを検知できる = ブランチカバレッジの計測は満たせる
    • 早期リターンや三項演算子みたいな書き方をしている場合は、条件分岐自体のコードが実行されればカバレッジを満たしてしまう = ブランチカバレッジの計測はできていない

ブランチカバレッジを計測する

READMEに描いてあるとおり、設定すればブランチカバレッジを計測することができるので試してみましょう。 https://github.com/simplecov-ruby/simplecov#branch-coverage-ruby--25

先ほど試した三項演算子で条件分岐するコード

class PlusOne
  attr_reader :num
  def initialize(attr)
    @num = attr
  end

  def call
   # initializeした時の引数が0以外なら1を足す、0なら例外(エラー)を発生させる
    @num != 0 ? (@num += 1) : raise
  end
end

テストは条件分岐がtrueになる場合のみ書きます。

require 'simplecov'
SimpleCov.start do
  enable_coverage :branch
end

RSpec.describe PlusOne do

  describe "callメソッドのテスト" do
    it "引数に1が渡されたplus_oneオブジェクトをcallすると@numの値が1増えること" do
      obj = PlusOne.new(1)
      obj.call
  
      expect(obj.num).to eq 2
    end
  end
end

この状態で実行すると、一行で記載された条件分岐に対するブランチカバレッジが測定できました。

このままじゃブランチカバレッジが50パーセントなので、三項演算子による条件分岐を全てテストするように追加しましょう。

require 'simplecov'
SimpleCov.start do
  enable_coverage :branch
end

RSpec.describe PlusOne do

  describe "callメソッドのテスト" do
    it "引数に1が渡されたplus_oneオブジェクトをcallすると@numの値が1増えること" do
      obj = PlusOne.new(1)
      obj.call
  
      expect(obj.num).to eq 2
    end

    it "引数に0が渡されたplus_oneオブジェクトをcallするとエラーが発生すること" do
      obj = PlusOne.new(0)

      expect { obj.call }.to raise_error(RuntimeError)
    end
  end
end

これで無事ブランチカバレッジも100パーセントになることを確認できました。

ブランチカバレッジ計測の際の注意点

READMEに記載されているようにブランチカバレッジは条件分岐のみを対象としているため、例えば条件分岐のないファイルに関してはカバレッジが100パーセントと報告されるらしいので試してみましょう。

class Piyo
  def self.call
    puts 'piyo'
  end
end

新規ファイルのみ作成した状態でテストを書かずに実行してみます。

テストを書いていないので、ラインカバレッジは100パーセントでは無いのは当然ですが、ブランチカバレッジが100パーセントになっていますね🤔

つまりREADMEに記載の通り、ブランチカバレッジだけではカバレッジ計測として使えず、ラインカバレッジと合わせて確認する必要があるということが分かりました。

privateメソッドはどうなるのか?

最後に少し気になったのでprivateメソッドを追加して計測してみます。

class PlusOne
  attr_reader :num
  def initialize(attr)
    @num = attr
  end

  def call
   # initializeした時の引数が0以外なら1を足す、0なら例外(エラー)を発生させる
    @num != 0 ? (@num += 1) : raise
    double
  end

  private

  def double
    @num *= 2
  end
end

計測結果を見たところ、実際にそのprivateメソッドを呼び出す処理のテストを書いていれば、ラインカバレッジとして計測されていることが分かりました。

まとめ

もしチームでテストカバレッジを測るツールの導入を検討されている時に「SimpleCovってどれくらいのカバレッジを計測できるの?」みたいなことを聞かれた際は、この記事を参考にしていただければ幸いです。


マネーフォワードでは、エンジニアを募集しています。 ご応募お待ちしています。

【会社情報】 ■Wantedly株式会社マネーフォワード福岡開発拠点関西開発拠点(大阪/京都)

【SNS】 ■マネーフォワード公式noteTwitter - 【公式】マネーフォワードTwitter - Money Forward Developersconnpass - マネーフォワードYouTube - Money Forward Developers