この記事は 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】 ■マネーフォワード公式note ■Twitter - 【公式】マネーフォワード ■Twitter - Money Forward Developers ■connpass - マネーフォワード ■YouTube - Money Forward Developers