テストコードでループを使うことの是非について考える
こんにちは、いっせいです。
先日レビューをしているとテストコード内でループをつかっているものがありました。
頭の片隅に「テストコードではループは使わないほうがよい」という記憶があったのですが、その出典を示すことができずもやもやしていました。
そこで自分のためにも整理のために考えてみたいと思います。
対象のサンプルコード
宛先を指定してインスタンスを生成する案内クラスを考えてみます
class Notice
def initialize(send_for)
@send_for = send_for
end
def title
case @send_for
when 'employee' then
'従業員のみなさまへ'
when 'personnel' then
'人事のみなさまへ'
when 'doctor' then
'産業医のみなさまへ'
else
'みなさまへ'
end
end
end
1つのテストで1つの検証
https://www.betterspecs.org/#single でもある通り
「各テストではひとつの検証を行うべき」
とされています。
これは
- どのような仕様なのかがわかりにくくなる
- 途中の検証が失敗した場合、以降の検証が成功するかわからない。 -> バグを特定する情報が減ってしまう
ということがあると思います。
この方針に則ってテストコードを書くと以下のようになります。
require 'rails_helper'
describe Notice do
let(:notice) { Notice.new(send_for) }
describe '#title' do
subject { notice.title }
context 'send_forが従業員の時' do
let(:send_for) { 'employee' }
it '「従業員のみなさまへ」と返ってくる' do
expect(subject).to eq '従業員のみなさまへ'
end
end
context 'send_forが人事の時' do
let(:send_for) { 'personnel' }
it '「人事のみなさまへ」と返ってくる' do
expect(subject).to eq '人事のみなさまへ'
end
end
context 'send_forが産業医の時' do
let(:send_for) { 'doctor' }
it '「産業医のみなさまへ」と返ってくる' do
expect(subject).to eq '産業医のみなさまへ'
end
end
context 'send_forがその他の時' do
let(:send_for) { 'other' }
it '「みなさまへ」と返ってくる' do
expect(subject).to eq 'みなさまへ'
end
end
end
end
実行すると
Notice
#title
send_forが従業員の時
「従業員のみなさまへ」と返ってくる
send_forがその他の時
「みなさまへ」と返ってくる
send_forが産業医の時
「産業医のみなさまへ」と返ってくる
send_forが人事の時
「人事のみなさまへ」と返ってくる
Finished in 1.32 seconds (files took 0.74873 seconds to load)
4 examples, 0 failures
となります。
確かに同じような記述があるので共通化しループしたくなる気持ちもわかります。
試しにループで書いてみます
describe Notice do
describe '#title' do
subject { notice.title }
it '宛先別に件名が適切に返ってくること' do
data_pairs = [
['employee', '従業員のみなさまへ'],
['personnel', '人事のみなさまへ'],
['doctor', '産業医のみなさまへ'],
['other', 'みなさまへ']
]
data_pairs.each do |arg, expectation|
notice = Notice.new(arg)
expect(notice.title).to eq expectation
end
end
end
end
実行すると
Notice
#title
宛先別に件名が適切に返ってくること
Finished in 1.19 seconds (files took 0.71437 seconds to load)
1 example, 0 failures
無理やり感がすごいですね。。。
たしかにコードは少なくなりましたが、実行結果がわかりづらくなりました。
「宛先別に件名が適切に返ってくること」だとこのメソッドがどんな宛先の時にどのような件名を返すかわかりません。
結局ループを使って「ひとつのテストで複数の検証を行う」をしているに過ぎません。
ループを使いつつ「1つのテストで1つの検証」を行う
そこで最初のテストコードの context のまとまりごとにループさせてみます。
require 'rails_helper'
describe Notice do
let(:notice) { Notice.new(send_for) }
describe '#title' do
subject { notice.title }
data_pairs = [
['従業員', 'employee', '従業員のみなさまへ'],
['人事', 'personnel', '人事のみなさまへ'],
['産業医', 'doctor', '産業医のみなさまへ'],
['その他', 'other', 'みなさまへ']
]
data_pairs.each do | pattern, arg, expectation|
context "send_forが#{pattern}の時" do
let(:send_for) { arg }
it "「#{expectation}」と返ってくる" do
expect(subject).to eq expectation
end
end
end
end
end
実行結果は
Notice
#title
send_forがその他の時
「みなさまへ」と返ってくる
send_forが従業員の時
「従業員のみなさまへ」と返ってくる
send_forが産業医の時
「産業医のみなさまへ」と返ってくる
send_forが人事の時
「人事のみなさまへ」と返ってくる
Finished in 1.21 seconds (files took 0.69544 seconds to load)
4 examples, 0 failures
テストコードが短くなって、出力は最初のテストコードと同様の結果となりました。
これであればtitleのパターンが追加されたらdata_pairsの値を増やせばいいので楽そうです。
まとめ
このケースはcontextごとループで実行すればよいかなと思いました。
しかし都合がいいサンプルコードになってしまった気もしないではないです。
「こういうケースであればループは使わないほうがわかりやすい」
「そもそもこういった理由でループは使うべきではない」
などご意見がございましたらぜひ @ise_tang までご連絡ください!
よりよいテストコード書いていきましょう!それでは!