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