テストコードでループを使うことの是非について考える | Dev Driven 開発・デザインチーム テストコードでループを使うことの是非について考える | 働くひとと組織の健康を創る iCARE

BLOG

テストコードでループを使うことの是非について考える

中村一星
2021/08/31

こんにちは、いっせいです。

先日レビューをしているとテストコード内でループをつかっているものがありました。

頭の片隅に「テストコードではループは使わないほうがよい」という記憶があったのですが、その出典を示すことができずもやもやしていました。
そこで自分のためにも整理のために考えてみたいと思います。

対象のサンプルコード

宛先を指定してインスタンスを生成する案内クラスを考えてみます

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 までご連絡ください!

よりよいテストコード書いていきましょう!それでは!