Rails 開発で RSpec を使用していると、Factorybot等で作成したオブジェクトやカラムに対してのバリデーションを一時的に無視したい時が現れます。
たとえばどういう時かというと、バリデーションを設定したときに、
- 外部依存のAPIから取得すべき必須パラメータがまだモックできていない
- CSV取り込み用の途中データをテストしたい
というときに、テストケースが全て落ちてしまいますよね。
そのようなときにうまくいった方法を書きます。
skip_callback
を使用する
特定のバリデーションが、before_validation
で設定されていた場合、skip_callback
を使用することで、バリデーションを一部スキップすることが可能です。
簡単なコードで表すと、
class Order < ApplicationRecord
before_validation :validate_external_status
private
def validate_external_status
# 外部APIでのステータス検証
ExternalService.check(status_code)
end
end
というモデルがあったとして、
RSpec.describe "Order" do
let(:order) { build(:order, status_code: "PENDING") }
context "外部ステータス検証をスキップしたいとき" do
before do
# validate_external_status コールバックだけを無効化
Order.skip_callback(:validate, :before, :validate_external_status)
end
after do
# コールバックを復元
Order.set_callback(:validate, :before, :validate_external_status)
end
it "バリデーションなしで保存できる" do
expect(order.save).to be true # passed
end
end
end
という具合に使用します。
他のテストへの影響を最小限にするため、テスト後に必ず set_callback
で元へ戻すことで、一部のバリデーションをスキップし、他は通常通りテストを行うようにします。
FactoryBotの trait やオプションでバリデーションをスキップ
FactoryBot のtrait
やto_create
オプションを組み合わせると、テスト用のファクトリでのみ save(validate: false)
を実行するようにカスタマイズできます。
基本構文は、
trait :skip_validations do
to_create { |instance| instance.save(validate: false) }
end
です。
name と email が必要な User モデルがある前提で使用してみると、
FactoryBot.define do
factory :user do
name { "ohtani" }
email { "invalid_email" } # 本来は format: のバリデーションがあると仮定
trait :skip_validations do
to_create { |user| user.save(validate: false) }
end
end
end
という感じになります。
この時点では何が起こるのかというと、
-
factory :user
のデフォルト生成では通常どおりバリデーションが働く -
create(:user, :skip_validations)
とするとバリデーションを飛ばして保存される
実際のテストでは、
RSpec.describe "User" do
let(:skipped_user) { create(:user, :skip_validations) } # バリデーションスキップ
context "skip_validations" do
it "persists the record without running validations" do
expect(skipped_user).to be_persisted # passed
end
end
end
というふうに使えます。
save(validate: false)
を使う
Rails の標準メソッド save(validate: false)
は、モデルに定義された すべての バリデーションと関連コールバックをスキップしてレコードを保存します。
RSpec.describe "User" do
let(:user) { User.new(email: "hogehogedaze") } # 不正なメールフォーマット
context "skip validation" do
before { user.save(validate: false) }
it "persists the record even with invalid email" do
expect(user).to be_persisted
end
end
end
この方法は、Rails ガイドにも書いてあるように、save(validate: false)
は非常に注意して使用する必要があります。
なぜなら、この方法は、すべてのバリデーションを無視することになるためです。DB制約以外は何も担保してくれません。
結果としてテストのデータの整合性が保たれない不十分なものになってしまいます。そのため、注意して使う必要があります。
最後に
テストの開発効率を高めるためにバリデーションを一時的にスキップするテクニックは便利ですが、「本番コードを汚さない」「テストの意図を明確化する」ことが重要です。ぜひ、今回の事例を参考にしてみてください!