LoginSignup
8
5

More than 5 years have passed since last update.

原因不明のValidation Errorを、地獄の果てまで追い詰める

Last updated at Posted at 2018-05-18

テスト実行時など、「どこで、何ゆえ、Validationエラーなのか」わからないことがある。
特に、子データなどをnested_attributeで処理していたり、
フォーム項目が大量にあったり、
そもそもどのValidatorがいつ発動したか、すらわからなかったり。

最近でくわした凶悪なものでは、
- test環境で
- 「has_many through」先のモデルで、
- さらにその先の「belongs_to」で存在しないデータに関連がついていた
- Validationメッセージは「has_many through」の部分で「○○は不正です」
fixtureを足して解決できたわけだが…、頭ひねったところでムリゲ。

そんなところで使える強力なTips(独学)3つ。備忘録

どのモデルの、どの種類のValidation Errorでも、NGになったタイミングで捕まえる

問答無用で、デバグします。
ActiveModel::Errors
gems/activemodel-5.2.0/lib/active_model/errors.rb # L295あたり

   def add(attribute, message = :invalid, options = {})
      message = message.call if message.respond_to?(:call)
      detail  = normalize_detail(message, options)
      message = normalize_message(attribute, message, options)
      if exception = options[:strict]
        exception = ActiveModel::StrictValidationFailed if exception == true
        raise exception, full_message(attribute, message)
      end

      details[attribute.to_sym]  << detail
      messages[attribute.to_sym] << message
    end

↑ ここらへんは、「Validation実行後、NGくらってなんらかのメッセージが追加された」時に必ず通る。
なので、ここで byebug挿入。

> @base

で該当のModelの詳細を。

   300:       if exception = options[:strict]
   301:         exception = ActiveModel::StrictValidationFailed if exception == true
(byebug) @base
#<City id: 980190962, code: "1", pref_code: "980190962", name: "新宿区", name_kana: "シンジュク", lon: 1.0, lat: 1.0, specialward: nil, creator_id: nil, updater_id: nil, deleter_id: nil, created_at: "2018-06-08 07:04:41", updated_at: "2018-06-08 07:04:41", deleted_at: nil>
(byebug) 
> where

で、タイミングを把握できる

冒頭のValidationは、↑のbreakpointを見張っていたら、
「ん? 変なModelで変なタイミングで引っかかってるな…」というので検出できた。

全部ログ出力してみる

以下のGemを使用する。
https://rubygems.org/gems/whiny_validation

出力イメージ

  Validation failed  #<Menu id: 77, name: "ヴェルサ", ..., tax_included: false, creator_id: nil, updater_id: nil, deleter_id: nil, created_at: nil, updated_at: nil, deleted_at: nil>
    => 率を入力してください
  Validation failed  #<AllianceProject id: nil, alliance_id: 999, project_id: 2, except_baby: false, kind: "limited", creator_id: 15, updater_id: 15, deleter_id: nil, created_at: nil, updated_at: nil, deleted_at: nil>
    => 具体メニューは不正な値です
  Validation failed  #<Alliance id: 999, name: "基本割引(料金調整用項目)", name_kana: "んんん", ..., creator_id: 1, updater_id: 15, deleter_id: nil, created_at: "2018-11-19 05:10:29", updated_at: "2018-11-19 05:10:29", deleted_at: nil>
    => 具体メニューは不正な値です
    => 単位を入力してください
    => 回収方法を入力してください
    => Alliance projectsは不正な値です
   (1.1ms)  ROLLBACK

2, 3モデル先でも、きっちり受け止めてくれるみたいですね。

Validationメッセージ自体は、i18n後の文字列(errors.full_messages)。
列名と箇所を判明したいので、full_messagesよりかは、
列キーとハッシュがいいな。

ドキュメントがほぼほぼないが、カスタマイズ方法模索中。

Testで、Validationの詳細をチェックする

minitest で controller_testなら、以下のテストを追加しておく

      assert_equal({}, assigns[:customer].errors.messages.select { |_, v| v.present? })
F

Failure:
CustomersControllerTest#test_should_create_customer [/home/ec2-user/environment/base2/test/controllers/customers_controller_test.rb:22]:
Expected: {}
  Actual: {:status=>["を入力してください"]}

どのフィールドが、何のValidationで落ちたか、を、テスト結果で確認できる
テスト結果で即わかるので、対応が楽。

毎回実装方法を忘れるので、
controller_test(今はfunctional_testというのか)のテンプレに格納。
rails g系で自動生成させます

以下、ご参考までに。

{root}/lib/templates/test_unit/scaffold/functional_test.rb

require 'test_helper'

<% module_namespacing do -%>
class <%= controller_class_name %>ControllerTest < ActionDispatch::IntegrationTest
  <%- if mountable_engine? -%>
  include Engine.routes.url_helpers
  <%- end -%>
  include Devise::Test::IntegrationHelpers

  setup do
    @<%= singular_table_name %> = <%= fixture_name %>(:one)
    sign_in users(:one)
  end

  test 'should get index' do
    get <%= index_helper %>_url
    assert_response :success
  end

  test 'should get new' do
    get <%= new_helper %>
    assert_response :success
  end

  test 'should create <%= singular_table_name %>' do
    assert_difference('<%= class_name %>.count') do
      post <%= index_helper %>_url, params: { <%= "#{singular_table_name}: {" %>
<% attributes_hash.each do |field, row| -%>
<% next if field.in? %w[id creator_id created_at updater_id updated_at deleter_id deleted_at] -%>
        <%= "#{field}: #{row}," %>
<% end %>
      <%= '}' %> }

      assert_equal({}, assigns[:<%= singular_table_name %>].errors.messages.select { |_, v| v.present? })
    end

    assert_redirected_to <%= singular_table_name %>_url(<%= class_name %>.last)
  end

  test 'should show <%= singular_table_name %>' do
    get <%= show_helper %>
    assert_response :success
  end

  test 'should get edit' do
    get <%= edit_helper %>
    assert_response :success
  end

  test 'should update <%= singular_table_name %>' do
    patch <%= show_helper %>, params: { <%= "#{singular_table_name}: {" %>
<% attributes_hash.each do |field, row| -%>
<% next if field.in? %w[id creator_id created_at updater_id updated_at deleter_id deleted_at] -%>
        <%= "#{field}: #{row}," %>
<% end %>
    <%= '}' %> }

    assert_equal({}, assigns[:<%= singular_table_name %>].errors.messages.select { |_, v| v.present? })
    assert_redirected_to <%= singular_table_name %>_url(<%= "@#{singular_table_name}" %>)
  end

  test 'should destroy <%= singular_table_name %>' do
    assert_difference('<%= class_name %>.count', -1) do
      delete <%= show_helper %>
    end

    assert_redirected_to <%= singular_table_name %>_url(<%= "@#{singular_table_name}" %>)
  end
end
<% end -%>
8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5