はじめに
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
のキーごとに answers
を each_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 になっていれば、どのメソッドまで実行したかはわかるのでなんとかなります。