ruby-on-rails 如何测试自定义验证器?

xdyibdwo  于 2022-12-15  发布在  Ruby
关注(0)|答案(7)|浏览(186)

我有以下验证器:

# 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

我希望能够在我的lib目录中的RSpec中测试这个。到目前为止的问题是我不确定如何初始化一个EachValidator

v64noz0r

v64noz0r1#

我不是另一种方法的狂热爱好者,因为它把测试和实现联系得太紧密了。而且,它相当难以遵循。这是我最终使用的方法。请记住,这是我的验证器实际上所做的一个严重的过度简化......只是想更简单地演示它。肯定有优化要做

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”,这样我们就可以实际测试它的结果,而不是对实现的每个部分都有期望

fumotvh3

fumotvh32#

这是我为那个文件设计的一个快速规范,它运行得很好。我想可能会清理掉存根,但希望这足以让你开始。

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
z18hc3ub

z18hc3ub3#

我建议创建一个匿名类用于测试目的,例如:

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,它可以在多个规范中引用,导致由于不相关的验证之间的交互而导致意外和难以调试的行为,您正在尝试隔离测试这些行为。

8tntrjer

8tntrjer4#

受“Gazler”回答的启发,我得出了以下结论:模拟模型,但使用ActiveModel::Errors作为错误对象。这大大减少了模拟。

require 'spec_helper'

RSpec.describe EmailValidator, type: :validator do
  subject { EmailValidator.new(attributes: { any: true }) }

  describe '#validate_each' do
    let(:errors) { ActiveModel::Errors.new(OpenStruct.new) }
    let(:record) {
      instance_double(ActiveModel::Validations, errors: errors)
    }

    context 'valid email' do
      it 'does not increase error count' do
        expect {
          subject.validate_each(record, :email, 'test@example.com')
        }.to_not change(errors, :count)
      end
    end

    context 'invalid email' do
      it 'increases the error count' do
        expect {
          subject.validate_each(record, :email, 'fakeemail')
        }.to change(errors, :count)
      end

      it 'has the correct error message' do
        expect {
          subject.validate_each(record, :email, 'fakeemail')
        }.to change { errors.first }.to [:email, 'is not an email']
      end
    end
  end
end
flmtquvp

flmtquvp5#

还有一个例子,扩展一个对象而不是在规范中创建新类。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
u2nhd7ah

u2nhd7ah6#

使用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
sqxo8psd

sqxo8psd7#

如果可以不使用存根,我更喜欢这种方式:

require "rails_helper"

describe EmailValidator do
  let(:user) { build(:user, email: email) } # let's use any real model
  let(:validator) { described_class.new(attributes: [:email]) } # validate email field

  subject { validator.validate(user) }

  context "valid email" do
    let(:email) { "person@mail.com" }

    it "should be valid" do
      # with this expectation we isolate specific validator we test
      # and avoid leaking of other validator errors rather than with `user.valid?`
      expect { subject }.to_not change { user.errors.count } 
      expect(user.errors[:email]).to be_blank
    end
  end

  context "ivalid email" do
    let(:email) { "invalid.com" }

    it "should be invalid" do
      expect { subject }.to change { user.errors.count }
      # Here we can check message
      expect(user.errors[:email]).to be_present
      expect(user.errors[:email].join(" ")).to include("Email is invalid")
    end
  end
end

相关问题