🦁

RSpec の not_to change で ActiveRecord のオブジェクトを参照するときに注意する

2024/03/23に公開

次のようなメソッドのテストを書くときに

class User < ActiveRecord::Base
  def update_name!(name)
    update!(name:) if name.present?
  end
end

次のようなテストを RSpec で記述する事ができます。

RSpec.describe User do
  describe "#update_name" do
    subject { user.update_name!(name) }

    let(:user) { User.create(name: "mami") }

    context "'homu' を渡した場合" do
      let(:name) { "homu" }
      # レコードの値が更新される
      it { expect { subject }.to change { user.reload.name }.from("mami").to("homu") }
    end

    context "'' を渡した場合" do
      let(:name) { "" }
      # レコードの値は更新されない
      it { expect { subject }.not_to change { user.reload } }
    end

    context "nil を渡した場合" do
      let(:name) { nil }
      # レコードの値は更新されない
      it { expect { subject }.not_to change { user.reload } }
    end
  end
end

しかし、上記のテストでは『レコードの値が更新れない』ことの動作を担保する事はできません。
例えば以下のように実装を変えてもテストは落ちません。

class User < ActiveRecord::Base
  def update_name!(name)
    # 常に更新するようにする
    update!(name:) # if name.present?
  end
end

これは change マッチャで ActiveRecord のオブジェクトが『変わっているか変わっていないか』をテストしているからです。

# user.reload の結果が変わっていないことを担保するテスト
it { expect { subject }.not_to change { user.reload } }

ActiveRecord のオブジェクトの同一性は『属性の値が同じかどうか』ではなくて『レコードの id の値が同じかどうか』で担保されています。
なので『属性の値が変わっていても』 id の値が同じであれば『変わっていない』と判定されます。
今回のように『属性の値が変わっていないことを担保する』のであれば以下のように #attributes かもしくは直接『 name の値を参照する』事によって意図するテストとして担保する事ができます。

RSpec.describe User do
  describe "#update_name" do
    subject { user.update_name!(name) }

    let(:user) { User.create(name: "mami") }

    context "'homu' を渡した場合" do
      let(:name) { "homu" }
      # レコードの値が更新される
      it { expect { subject }.to change { user.reload.name }.from("mami").to("homu") }
    end

    context "'' を渡した場合" do
      let(:name) { "" }
      # name の値が変更されていないことをテストする
      it { expect { subject }.not_to change { user.reload.name } }
    end

    context "nil を渡した場合" do
      let(:name) { nil }
      # 属性の値が変更されていないことを担保する
      it { expect { subject }.not_to change { user.reload.attributes } }
    end
  end
end
GitHubで編集を提案

Discussion