はじめに
Rails Tips の 5 回目です。連日の model のテストを簡単に書くための shared_example の紹介の続きです。しばらくは私の Shared example の雛形 (lib/templates/rspec/model/model_spec.rb
)を一つずつ解説していく形です。
require "rails_helper" # RSpec.describe <%= class_name %>, type: :model do # let(:<%= singular_table_name %>) { <%= plural_table_name %> :<%= singular_table_name %> } # let(:can_delete) { <%= plural_table_name %> :can_delete } # context "属性に関する共通テスト" do # subject { can_delete } # let(:another_object) { <%= plural_table_name %> :another_object } # it_behaves_like "存在制約", %i[] # it_behaves_like "一意制約", %i[] # it_behaves_like "複合一意制約", %i[] # it_behaves_like "削除可能制約" # it_behaves_like "削除不可制約" # it_behaves_like "関連確認", :<%= singular_table_name %>, has_many: %i[], has_one: %i[], children: :optional, child: :optional # it_behaves_like "親削除時に自分も削除", :<%= singular_table_name %>, %i[has_many has_one] # it_behaves_like "親は削除不可", :<%= singular_table_name %>, %i[has_many has_one] # it_behaves_like "親削除時にnullを設定", :<%= singular_table_name %>, %i[has_many has_one] # end
削除可能制約・削除不可制約
データが削除可能か不可能かをチェックする shared_example です。削除可能は制約なのかという話もありますが、こちらだけ制約という言葉をつけないのもバランスが悪いので、削除可能制約としています。
基本的に何も条件がなければデータは削除可能なので、簡単に通ります。面倒なのは削除不可制約の方です。どんな場合でも削除が不可能にするのか、リレーションが設定されている時だけ削除不可なのかによってテストのやり方が変わってきます。
先日紹介した年度を示す Year は次の年度を作成することはありますが、削除することはありません。このため、このモデルは常に削除不可になります。
require "rails_helper" RSpec.describe Year, type: :model do let(:year) { years :y2023 } context "属性に関する共通テスト" do subject { year } it_behaves_like "削除不可制約" end end
一方、クラスを示す Kurasu は作ったばかりは削除できますが、学生が一人でも登録されると削除できなくなります。この場合、削除できる fixture と削除できない fixture を用意してどちらもテストをする必要があります。この Kurasu のテストの該当部分だけを抜粋すると以下のようになります。
require "rails_helper" RSpec.describe Kurasu, type: :model do let(:kurasu) { kurasu :k43 } context "属性に関する共通テスト" do context "without gnumbers" do subject { kurasus :can_delete } it_behaves_like "削除可能制約" end context "with gnumbers" do subject { kurasu } it_behaves_like "削除不可制約" end end
削除可能制約および削除不可制約の shared_example は非常に簡単で、subject.destroy の実行の前後でオブジェクトのカウントが変化するかどうかを確認するだけです。これを実現するための「削除可能制約」「削除不可制約」の shared example は以下のようになります。
shared_examples_for "削除可能制約" do it "削除できること" do klass = subject.class expect { subject.destroy }.to change(klass, :count).by(-1) end end shared_examples_for "削除不可制約" do it "削除できないこと" do klass = subject.class expect { subject.destroy }.not_to change(klass, :count) end end
この example では、通常の Proc の前後でクラスのオブジェクトのカウントを取得しています。削除可能制約ではカウントが1減るのに対し、削除不可制約ではカウントが減らないことを確認しています。
正しく動作させるための実装
常に削除できないモデルの場合には、 before_destroy
でエラーを追加するとともに、例外を発生させればよいです。Year モデルでは以下のように on_destroy_validation_method
を追加しています。このメソッドの中では、errors.add
でエラーメッセージを追加し、:abort
を throw
します。
# @note 年度は削除しない before_destroy :on_destroy_validation_method def on_destroy_validation_method errors.add :base, "年度は削除できません" throw :abort end private :on_destroy_validation_method
一方、kurasu は依存関係の削除制約で削除不可かどうかを制御します。dependent
が restrict_with_error
となっているため、gnumbers
が設定されている親は削除できないことになります。逆に学生が登録されていないクラスは、エラーが発生しないため削除できることになります。
# @return [ActiveRecord::Relation<Gnumber>] 対応する学生番号一覧 has_many :gnumbers, dependent: :restrict_with_error
おわりに
依存関係で削除不可にすることは多いので、よく削除可能制約の方が失敗することが多いです。このため、私の場合にはどのモデルにも can_delete というこの制約のテストのためだけの fixture を用意しています。この時、can_delete に間違えてリレーションを設定してしまうと、削除不可になってしまうので注意が必要です。