This is my take on isolating validations from ActiveRecord models, inspired by Corey Haines' gist here: https://gist.github.com/coreyhaines/3305349
The main desires are putting validation responsibility into their own object, reducing the bloat of god classes, and easing validation concerns from tests which should not care about them, particularly when validations are quite complex.
This method relies upon inclusion of the ValidationService module within an ActiveRecord class. The model will invoke its validator automatically while saving provided there is a validator that follows the appropriate naming convention.
In order to perform tests that don't concern themselves with validation logic that you desired to be unencumbered by validation, you have two options. One is to instantiate with a NilValidator: Account.new(validation_service: NilValidator)
Alternatively you can monkey-patch the validating class at the top of your test file:
AccountValidator
def validate; end
endThe latter approach may have negative impacts on other tests when using tools like Spork and may not be advisable in that scenario.
The loss of the ActiveModel helpers is noteworthy, but I feel not having them may steer away from greater trouble provided by such 'niceties'.
module ValidationService
def self.included(base)
base.class_eval do
attr_accessor :validation_service
after_initialize do
unless self.validation_service
self.validation_service = "#{self.class.name}Validator".constantize
end
end
validate do |record|
validation_service.new(record).validate
end
end
end
end
class NilValidator
attr_accessor :record
def initialize(record)
self.record = record
end
def validate
end
endclass Account < ActiveRecord::Base
include ValidationService
endclass BaseValidator
attr_accessor :record
def initialize(record)
self.record = record
end
def validate
before_validate
perform_validations
end
private
def perform_validations
raise NotImplementedError
end
def before_validate
clear_errors
end
def clear_errors
record.errors.clear
end
def value_of(attribute)
record.public_send(attribute)
end
def record_has?(attribute)
value_of(attribute).present?
end
endclass AccountValidator < BaseValidator
private
def perform_validations
presence_of :store_name
presence_of :contact_email
email_format_of :contact_email
end
def presence_of(attribute)
unless record_has? attribute
record.errors.add(attribute, :blank)
end
end
def email_format_of(attribute)
return unless record_has? attribute
if value_of(attribute) !~ /\A([^@\s]+)@((?:[-a-zA-Z0-9]+\.)+[a-zA-Z]{2,})\z/
record.errors.add(attribute, :invalid)
end
end
end