SmartHR Advent Calendar 2023 シリーズ2の1日目です。
最近、あるメソッドに対してテストケースが漏れなく書かれていることを保証するテストがほしいと思うことがあったので調べてみた記録を残します。
どういうときにそのようなテストが欲しくなるか、具体例を見ながら説明していきます。
具体例
ここに2つの数値と演算子となる文字列を渡すと計算結果を返してくれる.calc
メソッドを持つCalculatorクラスがあります。
現時点では足し算(plus
)、引き算(minus
)に対応しています。
class Calculator
AVAILABLE_OPERATORS = %w(plus minus)# 足し算、引き算のみできる
def self.calc(left, operator, right)
raise ArgumentError unless AVAILABLE_OPERATORS.include?(operator)
case operator
when 'plus'
left + right
when 'minus'
left - right
end
end
end
このメソッドに対するテストを以下のように書いたとしましょう。
RSpec.describe Calculator do
describe ".calc" do
context 'plus' do
it { expect(Calculator.calc(1, 'plus', 2)).to eq(3) }
end
context 'minus' do
it { expect(Calculator.calc(3, 'minus', 2)).to eq(1) }
end
context 'invalid operator' do
it { expect { Calculator.calc(1, 'invalid', 2) }.to raise_error(ArgumentError) }
end
end
end
すべての処理を通っておりカバレッジは100%です。よかったですね。
その後、追加で掛け算、割り算にも対応することになったので実装しました。
class Calculator
AVAILABLE_OPERATORS = %w(plus minus times divide) # 掛け算、割り算もできるようにした
def self.calc(left, operator, right)
raise ArgumentError unless AVAILABLE_OPERATORS.include?(operator)
case operator
when 'plus'
left + right
when 'minus'
left - right
when 'times'
left * right
when 'divide'
left / right
end
end
end
無事、掛け算や割り算を追加できちゃんと動くことも確認できました。おめでとうございます。
本題
しかし、ここからが本題。この実装をした時点でテストの追加を忘れていた場合でもテストはパスしてしまいます。コードレビューでも見逃してしまった場合、テストがない状態で本番にリリースされてしまいます。
また今回はケースが4つだけなので仮にすべてのテストケースが書いてあったときには書かれている!とわかりますが、ケースが数十にもなってくる場合、全てのテストケースを網羅できているかどうか目で見て判断することは難しくなってくると思います。
この抜け漏れの防止を仕組みで解決したいというのがモチベーションでした。
どうすればいいのか
調べたりChatGPTとお話したりCopilotに導かれるまま指を動かしてみたりしました。
するとRSpecにはメタデータを取得するメソッドが用意されており、それを使うことでやりたいことを実現できそうでした。
RSpec.describe Calculator do
describe ".calc" do
it 'すべての演算子のテストが存在すること' do
# テストのタイトルを取得する
example_descriptions = RSpec.current_example.example_group.children.map(&:description)
# => ["plus", "minus", "invalid operator"]
Calculator::AVAILABLE_OPERATORS.each do |operator|
expect(example_descriptions).to include(operator)
# => times、devideのテストがないのでfailedとなる
end
end
context 'plus' do
it { expect(Calculator.calc(1, 'plus', 2)).to eq(3) }
end
context 'minus' do
it { expect(Calculator.calc(3, 'minus', 2)).to eq(1) }
end
context 'invalid operator' do
it { expect { Calculator.calc(1, 'invalid', 2) }.to raise_error(ArgumentError) }
end
end
end
これで、演算子を追加したらテストの追加も強制することができるようになりました。
まとめ
他にもカバレッジをきちんと計測する方法など解決手段はいくつかあると思います。テストケースのテストをすることの是非についてはまだあまり考えられていないですが、少なくともこういったケースでは有効なこともあるんじゃないかと思っています。
今回の調査でRSpecのコードも少し読んだのですが、こんなメソッドもあるんだ便利〜とよい勉強にもなりました。