RailsでCustom validatorをテストする

TL;DR ActiveModel::Validationsをincludeした適当なクラスをつくれば良い。

Custom validatorはこういうやつ

例えば、DBのentriesテーブルのデータを扱うEntryモデルをつくっているとする。Entryモデルにはurlという属性が存在するが、urlにはHTTPかHTTPSでかつホスト名が存在するようなURLしか入れたくない。EntryモデルはActiveRecordを利用して実装されているが、標準で利用できるValidationだけではこの要求を実現できないと考える。

そこでCustom validatorと呼ばれる機能を利用し、独自のValidationのルールをつくる。独自でこれを行うには、モデルクラスに簡単なメソッドを定義して呼び出してもらう方法や、#validateメソッドを実装したクラスを用意する方法、#validate_eachメソッドを実装したクラスを用意する方法などがある。ここでは最後のものを利用する。ActiveModel::EachValidatorというのを継承すれば良いとのこと。

できたのが以下のもの。.validatesメソッドの引数に渡している:full_http_urlと上例で新たに定義したクラスの名前が一致しているので、勝手に呼び出してくれる。

```rb

Public: Validates if given value is valid and full HTTP URL.

Examples

class Entry < ActiveRecord::Base

validates :url, fullhttpurl: true

end

class FullHttpUrlValidator < ActiveModel::EachValidator def validateeach(record, attribute, value) begin uri = URI.parse(value) rescue URI::InvalidURIError uri = nil end if uri.nil? || uri.host.nil? || !uri.isa?(URI::HTTP) record.errors.add(attribute, :invalid_url) end end end ```

Custom validatorのテストを書く

それで表題の通りテストを書きたい訳だけれど、Entryクラスをそのままテストに利用してしまうのはあまり良い方針とは言えない。そこで、テスト用にダミーのモデルクラスをつくってテストすることにする。

ダミーのモデルクラスではEntryクラスと同じように.validatesを呼び出せるようなものにする必要がある。これには、クラス名が存在し(定数に挿入されているか.nameが呼び出せればOK)、かつActiveModel::Validationsがincludeされているクラスを用意すれば良い。テストのためにグローバルな名前空間にクラスを定義してしまうのは良くないから、適当にクラスオブジェクトをつくって変数か何かで扱う。クラスをつくるにはClass.newを使ってもいいけれど、外部からテスト用にURLを与える機能も実装したいので、丁度良さそうなStruct.newを使う。最終的にできたのが以下のコード。

```rb require 'spec_helper'

describe FullHttpUrlValidator do let(:model_class) do Struct.new(:url) do include ActiveModel::Validations

  def self.name
    'DummyModel'
  end

  validates :url, full_http_url: true
end

end

describe '#validate' do subject do model_class.new(url) end

context 'with empty string' do
  let(:url) do
    ''
  end
  it { should_not be_valid }
end

context 'with nil' do
  let(:url) do
    nil
  end
  it { should_not be_valid }
end

context 'without host' do
  let(:url) do
    'http:///about'
  end
  it { should_not be_valid }
end

context 'without http(s) scheme' do
  let(:url) do
    'ftp://example.com'
  end
  it { should_not be_valid }
end

context 'with https' do
  let(:url) do
    'https://example.com'
  end
  it { should be_valid }
end

context 'with full HTTP URL' do
  let(:url) do
    'http://example.com'
  end
  it { should be_valid }
end

end end ```

FullHttpUrlValidator #validate with empty string should not be valid with nil should not be valid without host should not be valid without http(s) scheme should not be valid with https should be valid with full HTTP URL should be valid