🦁
RSpec の not_to change で ActiveRecord のオブジェクトを参照するときに注意する
次のようなメソッドのテストを書くときに
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
Discussion