Как проверить пользовательский валидатор? - PullRequest
36 голосов
/ 12 октября 2011

У меня есть следующий валидатор:

# Source: http://guides.rubyonrails.org/active_record_validations_callbacks.html#custom-validators
# app/validators/email_validator.rb

class EmailValidator < ActiveModel::EachValidator
  def validate_each(object, attribute, value)
    unless value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
      object.errors[attribute] << (options[:message] || "is not formatted properly") 
    end
  end
end

Я хотел бы иметь возможность проверить это в RSpec внутри моего каталога lib.Проблема пока в том, что я не уверен, как инициализировать EachValidator.

Ответы [ 5 ]

67 голосов
/ 19 июля 2013

Я не большой поклонник другого подхода, потому что он слишком тесно связывает тест с реализацией.Кроме того, это довольно трудно следовать.Это подход, который я в конечном итоге использую.Пожалуйста, имейте в виду, что это грубое упрощение того, что на самом деле сделал мой валидатор ... просто хотел продемонстрировать это проще.Определенно необходимо выполнить оптимизацию

class OmniauthValidator < ActiveModel::Validator
  def validate(record)
    if !record.omniauth_provider.nil? && !%w(facebook github).include?(record.omniauth_provider)
      record.errors[:omniauth_provider] << 'Invalid omniauth provider'
    end
  end
end

Связанные спецификации:

require 'spec_helper'

class Validatable
  include ActiveModel::Validations
  validates_with OmniauthValidator
  attr_accessor  :omniauth_provider
end

describe OmniauthValidator do
  subject { Validatable.new }

  context 'without provider' do
    it 'is valid' do
      expect(subject).to be_valid
    end
  end

  context 'with valid provider' do
    it 'is valid' do
      subject.stubs(omniauth_provider: 'facebook')

      expect(subject).to be_valid
    end
  end

  context 'with unused provider' do
    it 'is invalid' do
      subject.stubs(omniauth_provider: 'twitter')

      expect(subject).not_to be_valid
      expect(subject).to have(1).error_on(:omniauth_provider)
    end
  end
end

В основном мой подход заключается в создании поддельного объекта "Validatable", чтобы мы могли фактически проверить результаты на нема не иметь ожидания для каждой части реализации

41 голосов
/ 12 октября 2011

Вот краткая спецификация, которую я выбрал для этого файла, и она хорошо работает.Я думаю, что заглушки, возможно, можно было бы убрать, но, надеюсь, этого будет достаточно, чтобы вы начали.

require 'spec_helper'


describe "EmailValidator" do

  before(:each) do
    @validator = EmailValidator.new({:attributes => {}})
    @mock = mock('model')
    @mock.stub("errors").and_return([])
    @mock.errors.stub('[]').and_return({})  
    @mock.errors[].stub('<<')
  end

  it "should validate valid address" do
    @mock.should_not_receive('errors')    
    @validator.validate_each(@mock, "email", "test@test.com")
  end

  it "should validate invalid address" do
    @mock.errors[].should_receive('<<')
    @validator.validate_each(@mock, "email", "notvalid")
  end  
end
6 голосов
/ 19 апреля 2017

Я бы рекомендовал создать анонимный класс для целей тестирования, таких как:

require 'spec_helper'
require 'active_model'
require 'email_validator'

RSpec.describe EmailValidator do
  subject do
    Class.new do
      include ActiveModel::Validations    
      attr_accessor :email
      validates :email, email: true
    end.new
  end

  describe 'empty email addresses' do
    ['', nil].each do |email_address|
      describe "when email address is #{email_address}" do
        it "does not add an error" do
          subject.email = email_address
          subject.validate
          expect(subject.errors[:email]).not_to include 'is not a valid email address'
        end
      end
    end
  end

  describe 'invalid email addresses' do
    ['nope', '@', 'foo@bar.com.', '.', ' '].each do |email_address|
      describe "when email address is #{email_address}" do

        it "adds an error" do
          subject.email = email_address
          subject.validate
          expect(subject.errors[:email]).to include 'is not a valid email address'
        end
      end
    end
  end

  describe 'valid email addresses' do
    ['foo@bar.com', 'foo@bar.bar.co'].each do |email_address|
      describe "when email address is #{email_address}" do
        it "does not add an error" do
          subject.email = email_address
          subject.validate
          expect(subject.errors[:email]).not_to include 'is not a valid email address'
        end
      end
    end
  end
end

Это предотвратит жестко закодированные классы, такие как Validatable, на которые можно ссылаться в нескольких спецификациях, что приведет к неожиданным и труднымповедение отладки из-за взаимодействия между несвязанными валидациями, которые вы пытаетесь протестировать изолированно.

4 голосов
/ 16 августа 2015

Используя отличный пример Neals в качестве основы, я придумал следующее (для Rails и RSpec 3).

# /spec/lib/slug_validator_spec.rb
require 'rails_helper'

class Validatable
  include ActiveModel::Model
  include ActiveModel::Validations

  attr_accessor :slug

  validates :slug, slug: true
end

RSpec.describe SlugValidator do
  subject { Validatable.new(slug: slug) }

  context 'when the slug is valid' do
    let(:slug) { 'valid' }

    it { is_expected.to be_valid }
  end

  context 'when the slug is less than the minimum allowable length' do
    let(:slug) { 'v' }

    it { is_expected.to_not be_valid }
  end

  context 'when the slug is greater than the maximum allowable length' do
    let(:slug) { 'v' * 64 }

    it { is_expected.to_not be_valid }
  end

  context 'when the slug contains invalid characters' do
    let(:slug) { '*' }

    it { is_expected.to_not be_valid }
  end

  context 'when the slug is a reserved word' do
    let(:slug) { 'blog' }

    it { is_expected.to_not be_valid }
  end
end
3 голосов
/ 12 августа 2015

Еще один пример с расширением объекта вместо создания нового класса в спецификации. BitcoinAddressValidator здесь настраиваемый валидатор.

require 'rails_helper'

module BitcoinAddressTest
  def self.extended(parent)
    class << parent
      include ActiveModel::Validations
      attr_accessor :address
      validates :address, bitcoin_address: true
    end
  end
end

describe BitcoinAddressValidator do
  subject(:model) { Object.new.extend(BitcoinAddressTest) }

  it 'has invalid bitcoin address' do
    model.address = 'invalid-bitcoin-address'
    expect(model.valid?).to be_falsey
    expect(model.errors[:address].size).to eq(1)
  end

  # ...
end
...