単一メソッド呼び出し shared_example: 小林研 Rails Tips (12)

はじめに

Rails Tips の 12 回目です。連日の model のテストを簡単に書くための shared_example の紹介の続きです。しばらくは私の Shared example の雛形 (lib/templates/rspec/model/model_spec.rb)を一つずつ解説していく形です。

#  describe "<%= class_name %> クラスについて" do
#    context "order_sort_order" do
#      subject { described_class.order_sort_order }
#      it_behaves_like "昇順確認", 0, ->(o) { o.sort_order }
#    end

#    context "order_bar_desc" do
#      subject { described_class.order_bar_desc }
#      it_behaves_like "降順確認", 99999, ->(o) { o.bar }
#    end
#
#    context "Class methods" do
#      subject { described_class }
#
#      it_behaves_like "単一メソッド呼び出し" do
#        let(:test_set) do
#          {
#              foo: [nil, [true, false, true]],
#          }
#        end
#      end
#    end

#    describe "get_or_create method" do
#      subject { -> { described_class.get_or_create(*params) }}
#      context '既存のものを取得' do
#        let(:params) { [object.xxx, object.yyy] }
#        it_behaves_like "オブジェクト数が変化しない?", <%= class_name %>
#      end
#
#      context '新規作成' do
#        let(:params) { [object.xxx, other.yyy] }
#        it_behaves_like "オブジェクトが1増えるか?", <%= class_name %>
#      end
#    end
#  end

単一メソッド呼び出し

今日は「単一メソッド呼び出し」になります。単一のオブジェクトに対してメソッドを呼び出して、その結果を確認する shared_example になります。subject を described_class に設定することで、クラスメソッドや scope のテストをすることができます。メソッドやパラメータを切り替えながらテストを並べるのは面倒なので、test_set でメソッド名と結果を一度に設定して一括でテストをします。

  let(:base_year) { years :y2019 }
  let(:next_year) { years :y2020 }

  context "Class methods" do
    subject { described_class }
    it_behaves_like "単一メソッド呼び出し" do
      let(:test_set) do
        {
          default: [nil, base_year],
          get_or_default: [[nil], base_year, next_year.id, next_year],
        }
      end
    end

test_set のハッシュはキーがメソッド名、対応する値は引数と答えが並んだ配列です。最初の default は引数の部分が nil なので引数なしで、その結果が base_year になります。これは以下のテストを実行しているのと同義になります。

it { expect(subject.default).to eq base_year }

次の get_or_default は二つのパターンをテストしています。引数が複数ある場合や、nil を渡したい時には配列で指定します。このテストは以下のテストを実行していることになります。明かに記述量が減っていることがわかります。

it { expect(subject.get_or_default nil).to eq base_year }
it { expect(subject.get_or_default next_year.id).to eq next_year }

この shared_example は以下のようになります。test_set のキーごとに answerseach_slice で切り取りながらテストを繰り返すだけです。ただし、大量に実行するとどのテストで失敗したのかわからないので、documentation_formatter の時のみメソッド名を表示しています(progress_formatter の時には表示しません)。

shared_examples_for "単一メソッド呼び出し" do
  it "上述のメソッドの結果が正しいこと" do
    test_set.each do |method, answers|
      print "\tmst: #{method}\n" if documentation_formatter?
      answers.each_slice(2) do |(v, a)|
        expect(subject.send(method, *v)).to eq a
      end
    end
  end
end

正しく動作させるための実装

それぞれの実装は単にクラスメソッドを用意するだけです。default はこんなコードになっています。初期状態で存在しない時に作成するものなどが入っているので少し長いです。

  # @return [Year] データベースに記録されているデフォルト年度
  def self.default
    default_year = Year.find_by default_year: true
    unless default_year
      date = Time.zone.today
      y = date.year
      m = date.month
      y -= 1 if m < 4
      default_year = Year.year_value_is(y).take
      default_year ||= Year.new year: y, default_year: true
      default_year.default_year = true
      default_year.save
    end
    default_year
  end

get_or_default も単なるクラスメソッドです。

  # @param [Integer, nil] yid 年度id
  # @return [Year] yid で指定された Year または Year.default
  def self.get_or_default(yid)
    yid && find(yid) || Year.default
  end

おわりに

単一メソッド呼び出しは大量のテストを書くのが面倒だったので、テストを手抜きするために作ったものです。クラスメソッドのテストにちょうどいいので、こんな感じで使っています。手抜きをしている分、エラーになった時にどこで失敗したのがわかりにくい点はありますが、documentation_formatter になっていれば、どのメソッドまで実行したかはわかるのでなんとかなります。