using spec to test a validator

how to test a custom rails validator using rspec

given rails4 and rspec3, here is a tidy way to test your custom validator:

# spec/validators/domain_name_validator_spec.rb

require 'spec_helper'

class DomainNameValidatable
  include ActiveModel::Validations
  attr_accessor  :domain_name
  validates :domain_name, domain_name: true
end

describe DomainNameValidator do

  subject { DomainNameValidatable.new }

  good = {
    'with proper domain name' => ['zeebar.com', 'www.zeebar.com']
  }

  good.each do |title, good_domain_names|
    good_domain_names.each do |good_domain_name|
      context "#{title} #{good_domain_name}" do
        it 'is valid' do
          subject.stubs(domain_name: 'zeebar.com')
          expect(subject).to be_valid
        end
      end
    end
  end

  bad = {
    'without domain' => nil,
    'with blank domain' => '',
    'with mixed case domain' => 'Zeebar.com',
    'with short wonky domain' => 'twitter',
    'with spacey wonky domain' => 'zeebar.com '
  }

  bad.each do |title, bad_domain_name|

    context "#{title} '#{bad_domain_name}'" do
      it 'is invalid' do
        subject.stubs(domain_name: bad_domain_name)
        expect(subject).to_not be_valid
        expect(subject.errors[:domain_name].size).to eq 1
      end
    end

  end

end

remember to add app/validators to your application.config, like to:

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

and of course you will need the validator itself:

# app/validators/DomainNameValidator.rb

class DomainNameValidator < ActiveModel::EachValidator

  # is not nearly as permissible as it probably should be
  DOMAIN_NAME_REGEX = /\A[a-z][a-z0-9]+(\.[a-z][a-z0-9]+)+\z/i

  def validate_each(record, attribute, value)
    if value
      if value.downcase != value
        record.errors[attribute] << (options[:message] || "domain names should be lowercase")
      else
        unless value =~ DOMAIN_NAME_REGEX
          record.errors[attribute] << (options[:message] || "does not look like a domain name, example: zeebar.com")
        end
      end
    else
      record.errors[attribute] << (options[:message] || "missing value")
    end
  end
end

note that, since rspecs are ruby, you can Dry your test rig using whatever hash/table structure best suites your case topology.