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

Ruby on rails 如何测试自定义验证器?,ruby-on-rails,testing,rspec,validation,activemodel,Ruby On Rails,Testing,Rspec,Validation,Activemodel,我有以下验证器: # 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) un

我有以下验证器:

# 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
#来源:http://guides.rubyonrails.org/active_record_validations_callbacks.html#custom-验证器
#app/validators/email_validator.rb
类EmailValidatorobject.errors[attribute]这里是我为该文件编写的一个快速规范,它运行良好。我认为存根可能会被清理,但希望这将足以让你开始

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
require'spec\u helper'
请描述“EmailValidator”的用途
在…之前做
@validator=EmailValidator.new({:attributes=>{})
@mock=mock('模型')
@mock.stub('errors')。和_返回([])
@mock.errors.stub('[]')和_return({})

@mock.errors[]存根(“我不太喜欢另一种方法,因为它将测试与实现联系得太近了。而且,这也很难遵循。这是我最终使用的方法。请记住,这是对我的验证器实际操作的过度简化…只是想更简单地演示一下。肯定有优化拟作出的修订

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

基本上,我的方法是创建一个伪对象“validable”,这样我们就可以实际测试它的结果,而不是对实现的每个部分都有期望。再举一个例子,扩展一个对象,而不是在规范中创建新类。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
require'rails\u helper'
模块位寻址测试
def自我扩展(父级)

类使用Neals伟大的示例作为基础,我提出了以下内容(对于Rails和RSpec3)


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

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

这将防止硬编码类,如
validable
,它们可以在多个规范中引用,由于不相关的验证之间的交互而导致意外和难以调试的行为,您正试图单独测试这些行为。

受@Gazler的答案启发,我提出了以下建议:模拟模型,但是t使用
ActiveModel::Errors
作为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

很好。不知道mock(‘model’)我将尝试了解更多信息。这是旧的RSpec文档,但模拟构造函数保持不变。模型只是模拟的标识符。你也可以用工厂在功能上实现它。我认为工厂太过分了。我们只需要一个空类来包含错误数组。在更新版本的Rails上(我使用的是4.1)您需要为验证器指定一些属性,以避免出现ArgumentError—只要属性不为空,传入什么并不重要,因此类似于
@validator=EmailValidator.new({:attributes=>{:foo=>:bar})
会成功的。我喜欢这样做,因为我喜欢通过ActiveModel::Validations模块测试验证。否则,你会将测试绑定到脆弱的ActiveModel实现上。对于Rails 4.1.6,我得到了“:attributes cannot be blank”在我更改validates_with调用以包含属性名称之前,例如使用OmniauthValidator验证属性:“omniauth_provider”
使用Rails 4.1.7,我能够绕过:属性不能为空
validates:omniauth\u provider出错,omniauth:true
嘿,我希望看到优化!关于验证测试的文档不多。很好,我认为这是一个比公认答案更干净的解决方案。您不需要包括
ActiveModel::Model
,而且创建一个非ymous类,例如
subject{class.new{include-ActiveModel::Validations;attr\u访问器:slug;validate:slug,slug:true}.new}
这是2020年的好答案(rspec-rails
4.0
)。
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
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