validator validar rails formularios campos asociaciones anidados ruby-on-rails testing rspec validation activemodel

ruby on rails - validar - ¿Cómo probar un validador personalizado?



validar campos rails (5)

Tengo el siguiente validador:

# 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

Me gustaría poder probar esto en RSpec dentro de mi directorio lib. El problema hasta ahora es que no estoy seguro de cómo inicializar un EachValidator .


Aquí hay una especificación rápida que noqueé para ese archivo y funciona bien. Creo que el trozo probablemente podría limpiarse, pero espero que esto sea suficiente para comenzar.

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", "[email protected]") end it "should validate invalid address" do @mock.errors[].should_receive(''<<'') @validator.validate_each(@mock, "email", "notvalid") end end


No soy un gran admirador del otro enfoque porque relaciona la prueba demasiado cerca de la implementación. Además, es bastante difícil de seguir. Este es el enfoque que finalmente uso. Tenga en cuenta que esto es una simplificación excesiva de lo que mi validador realmente hizo ... solo quería demostrarlo de manera más simple. Definitivamente hay optimizaciones para hacerse

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

Especificación asociada:

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

Básicamente mi enfoque es crear un objeto falso "Validatable" para que podamos probar los resultados en lugar de tener expectativas para cada parte de la implementación.


Recomendaría crear una clase anónima para fines de prueba, como:

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'', ''@'', ''[email protected].'', ''.'', '' ''].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 [''[email protected]'', ''[email protected]''].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

Esto evitará que las clases codificadas como Validatable , que pueden ser referenciadas en múltiples especificaciones, muestren un comportamiento inesperado debido a las interacciones entre validaciones no relacionadas, que usted está tratando de probar de forma aislada.


Un ejemplo más, con la extensión de un objeto en lugar de crear una nueva clase en la especificación. BitcoinAddressValidator es un validador personalizado aquí.

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


Utilizando el gran ejemplo de Neals como base, se me ocurrió lo siguiente (para Rails y 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