【Rails】【RSpec】【shoulda-matchers】validate_uniqueness_ofのscoped_toではまったときのまとめ
目的
shoulda-matchersでモデルのバリデーションのテストを書いていたときに、Railsのアトリビュートメソッドのキャッシュの動きで若干はまったため、簡単にまとめる。
shoulda-matchersの簡単な導入
shoulda-matchersはgemで、RSpecやMinitestにvalidate_presence_of
などのワンライナーのテストのシンタックスを追加するもの。
詳しい話、やその他のテストメソッドに関しては、本家のreadme参照。(知らない機能がたくさんある!)
Railsのアトリビュートメソッドのキャッシュ機能
メタプログラミングRubyを読んでいただいた方はわかるかもしれないが、Rails4系・5系のコードはメソッドの定義をキャッシュすることでパフォーマンスの向上を図っている。
詳しい実装には触れないが、具体的には以下のような動作になる(Rails 5.0.0.1, Ruby 2.3.1)
2.3.1 (main):0 > User.method_defined?(:name) => false 2.3.1 (main):0 > User.new => #<User id: nil, name: nil, role_id: nil, created_at: nil, updated_at: nil, email: ""> 2.3.1 (main):0 > User.method_defined?(:name) => true
shoulda-matchersのvalidate_uniqueness_ofメソッドのscoped_toチェーン
モデルでのバリデーションを以下のように書く
class Phone < ActiveRecord::Base validates :phone, uniqueness: true end
shoulda-matchersでテストを書くと以下のように書ける
require 'rails_helper' describe Phone do it { is_expected.to validate_uniqueness_of(:phone) } end
さらに、複数カラムでユニークになるようにバリデーションをすることもでき
class Phone < ActiveRecord::Base validates :phone, uniqueness: { scope: :contact_id } end
shoulda-matchersで書くとこうなる
require 'rails_helper' describe Phone do it { is_expected.to validate_uniqueness_of(:phone).scoped_to(:contact_id) } end
しかし、上記のテストは(他のテストにもよるが、)コケる。
なぜテストが失敗するのか
以下のようなメッセージが出る
:contact_id does not seem to be an attribute on Phone.
ん?カラムあるよ?
これでしばらく進まなかった。
実際は、scoped_toで指定されたカラムは、以下のようにshoulda-matchersのなかでmethod_defined?
を使ってチェックしている。
def attribute_present_on_model? model.method_defined?("#{attribute}=") || model.columns_hash.key?(attribute.to_s) end
試しにbinding.pry
でとめ、contact_id=
を実行してみてからテストを流すとうまくいった。
メソッドのキャッシュのタイミングはバージョンによって異なるので、最新版であれば大丈夫かもしれません。
まとめ
感想
- gemのソースを読むのはなかなか楽しい。
- 今回のがバグなのかわからないが、最新版を使ってバグを踏んで、PR送ろう!