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
と上例で新たに定義したクラスの名前が一致しているので、勝手に呼び出してくれる。
# Public: Validates if given value is valid and full HTTP URL.
#
# Examples
#
# class Entry < ActiveRecord::Base
# validates :url, full_http_url: true
# end
#
class FullHttpUrlValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
begin
uri = URI.parse(value)
rescue URI::InvalidURIError
uri = nil
end
if uri.nil? || uri.host.nil? || !uri.is_a?(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を使う。最終的にできたのが以下のコード。
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