Edited at

RailsでCustom validatorをテストする

More than 5 years have passed since last update.

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