Railsでのカスタムバリデーション実装からテストまで

ruby_gems.jpg

最近、Railsのカスタムバリデーションを使って独自のバリデーションを追加したのだが、この作業事態全部をとっておきたいのでPOST。

大枠は全部@hanachin先生が教えてくださった。
毎度、本当にありがとうございます。
・バリデーションの実装は railsガイドの の通り。
・テストはこの記事を参考にした。

あ、今気づいたらサムネイルと内容あってない(*_*; まぁいいや。

目標

特定のモデルに、NGワードを設定しフォームから送信された内容に含まれていたら弾きたい。

例)User.nameに、「海老」が入ってたら
「”海老”は予約されている為使用できません」 とかね。

NGワードの辞書はDBで管理するほどのものでも無いので、テキストファイルにベタ書きで保存しておく。
(変更がある度にコミットしてマージしないといけないのが面倒になりそうだけど、そんなに頻度高くない予定)

NGワードの辞書を設置する

今回、テスト用のファイルとdevやproductionで使用するものを分けたかったので

  • テストは、 spec/fixtures/ng_words
  • dev, productionは、 lib/ng_words

に分けて設置した。
後々テストで、使用する単語だけ spec/fixtures/ng_wordsに入れて以下の様にした。

ng_word_sample
NG_ワード_サンプル

※ファイルの中身は、1行1ワードにしたテキストファイルでやんす。

バリデータの実装

app/validators というディレクトリを作り、そこに
ng_word_validator.rb というファイルで実装した。

class NgWordValidator < ActiveModel::EachValidator
  NG_WORDS_PATH = Rails.env.test? ? 'spec/fixtures/ng_words' : 'lib/ng_words'
  NG_WORDS = File.readlines(NG_WORDS_PATH).each{ |line| line.chomp! }

  def validate_each(record, attribute, value)
    NG_WORDS.each do |str|
      if value =~ /#{str}/
        record.errors[attribute] << (options[:message] || "の#{str}はNGワードです")
      end
    end
  end
end

NG_WORDSにセットしておいて、 validate_eachの引数の valueとマッチすればErrorに追加できるそう。
結構わかりやすい。

カスタムバリデータを読み込んでもらうようにする

autoload_pathsに追加する。

config/application.rb

config.autoload_paths += %W(#{config.root}/app/validators) 

モデルのvalidatesに追加する

実際にモデルでバリデーションしてもらうようにする

app/models/user.rb

validates :name, presence: true, ng_word: true

この、 “ng_word: true” だけで、今回追加したNgWordValidatorが有効になる。
便利。
これを必要なモデルのカラム全部に追加した。

テストの実装

テストは大きく、2つ。
・モデルのテストのバリデーション部分の追加
・カスタムバリデータそのもののテスト
を追加した。

モデルのテスト

spec/models/user_spec.rb

describe 'name' do
  <略>
  it { is_expected.to allow_value('ok_word_sample').for(:name) }
  it { is_expected.to_not allow_value('ng_word_sample').for(:name) }
end

これを、対象のカラム毎に追加。
別に allow_valueの引数は何でもいいので、もっとわかりやすいでも良かったかもしれない。

バリデータそのもののテスト

冒頭にも書いた、 この記事 をほぼ参考にしただけ。
spec/validators/ng_word_validator_spec.rbを掘りました。
モデルのカラムに入ってくる入力値は、 Struct.new(:input) で指定し、先ほど実装したバリデータを読み込み 指定できるそう。
こんな感じに。

require 'rails_helper'

RSpec.describe NgWordValidator do
  let(:model_class) do
    Struct.new(:input) do
      include ActiveModel::Validations

      def self.name
        'DummyModel'
      end

      validates :input, ng_word: true
    end
  end

  describe '#validate' do
    subject(:model) do
      model_class.new(input)
    end

    let(:full_messages) do
      model.valid?
      model.errors.full_messages
    end

    context 'NGワードでない文字は使用できる' do
      let(:input) do
        'ok_word_sample'
      end
      it { should be_valid }
    end

    context 'NGワードの文字は使用できない' do
      let(:input) do
        'ng_word_sample'
      end
      it { should_not be_valid }
    end

    context 'NGワードが含まれる文字列は使用できない' do
      let(:input) do
        '私はng_word_sampleです'
      end
      it { should_not be_valid }
    end

    context '複数のNGワードが含まれる文字列使用できない' do
      let(:input) do
        '私はng_word_sampleですし、NG_ワード_サンプルです。'
      end
      it { should_not be_valid }
    end

    context 'NGワードが含まれる改行付き文字列は使用できない' do
      let(:input) do
        "ok_word_sample\nng_word_sample"
      end
      it { should_not be_valid }
    end

    context 'NGワードが含まれる場合はメッセージが表示される' do
      let(:input) do
        'ng_word_sample'
      end
      specify do
        expect(full_messages).to include('Inputのng_word_sampleはNGワードです')
      end
    end

    context 'NGワードが複数含まれる場合は複数のメッセージが表示される' do
      let(:input) do
        '私はng_word_sampleですし、NG_ワード_サンプルです。'
      end
      specify do
        expect(full_messages).to include('Inputのng_word_sampleはNGワードです')
        expect(full_messages).to include('InputのNG_ワード_サンプルはNGワードです')
      end
    end
  end
end 

余談

NGワードの辞書ファイルに、空の行が1行混じっていたところ
これが、value =~ /#{str}/ の部分であらゆる入力にマッチしてしまって危うく事故るところだった。
辞書に空行は厳禁なのー。 
ん。 というかバリデータを修正すれば治る・・?(゜o゜)
いずれにせよ、辞書ファイルをテストと本番を分けているので危険が伴う。
とりあえず今日はおわり。

 

2015-08-25 | Posted in RailsNo Comments » 


関連記事

Comment





Comment



*