親削除時にnullを設定 shared_example: 小林研 Rails Tips (9)

はじめに

Rails Tips の 9 回目です。連日の 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

親削除時にnullを設定

今日は「親削除時にnullを設定」になります。属性に関する共通テストの最後です。こちらも読んで字の如しで、親が削除された時に関連が消えることを確認するテストです。校務支援システムでもそれほど例が多くないのですが、分かりやすそうな例を一つ紹介します。受検生 (jukensei) は学生 (gakusei) や入学情報 (gin) への関連を持っています。すでに学生を作成後に入学辞退などがあった時、学生を削除しますがそれによって受検生が消えてしまっては困ります。そこで、このテストが必要になります。

require "rails_helper"

RSpec.describe Jukensei, type: :model do

  context "属性に関する共通テスト" do
    subject { can_delete }
    it_behaves_like "関連確認", :jukensei, has_many: %i[chuugaku city gakusei student year], has_one: %i[gin]
    it_behaves_like "親削除時にnullを設定", :jukensei, %i[gakusei gin]
  end

この shared_example は relation の名前から subject の親を取り出し、その親を実際に削除します。その削除の際に親からの参照が null になることを確認しているだけです。

shared_examples_for "親削除時にnullを設定" do |model, relations|
  relations.each do |relation|
    context "#{model}.#{relation}を削除したとき" do
      it "#{model}.#{relation} が nil になること" do
        parent = subject.send(relation)
        parent.destroy
        expect(subject.reload.send(relation)).to be_nil
      end
    end
  end
end

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

親が null になれるためには、まず belongs_to で null が設定できる必要があります。そのために、optional が true である必要があります。上の場合だと、jukensei は以下のように設定します。すべての受検生は学生にリンクされるわけではないので、デフォルトでは null になっています。合格時に学生や入学情報が作成されリレーションが張られるということです。

  # @return [Gakusei] 対応する学生
  belongs_to :gakusei, optional: true
  # @return [Gin] 対応する入学情報
  belongs_to :gin, optional: true

ここで、入学辞退などで親である学生 (gakusei) を削除した時、jukensei に設定された gakusei_idnull にならなければなりません。このため、has_manydependent オプションに nullify を設定します。

  # @return [ActiveRecord::Relation<Jukensei>] 対応する受検生一覧
  has_many :jukenseis, dependent: :nullify

おわりに

この状況が発生するのは非常に稀ですが、これも間違えると重要な情報が消えるきっかけになるので必要なテストです。

今回で、「属性に関する共通テスト」は全部終了になります。モデルを作成した時には、上に紹介した雛形で書かれた it_behaves_like のコメントの塊がテストに用意されるようになっています。モデルに合わせて該当するコメントを外してテストをすることで、実装漏れをなすくことができます。明日からは scope に関する共通テストを紹介していきます。