Showing posts with label validatable. Show all posts
Showing posts with label validatable. Show all posts

Tuesday, May 01, 2007

Ruby: Validatable Tests

In the comments for my most recent entry, Validatable 1.2.2 released, Jon Leighton asks: how do you recommend testing validations?

I've had this discussion before when I wrote about testing ActiveRecord validations. I've not been able to come up with a 'best practice', but I've had some fun trying out different solutions.

My latest attempt at a solution alternative is to create assertions for the framework. For example, if I were to create a Validatable framework, that framework would include ValidatableAssertions.

To create validations for the Validatable framework I wrote my ideal syntax and then figured out how to make it work. The following code was the original version for validating that a class contained a presence of validation.
class FooTest < Test::Unit::TestCase
Foo.must_validate.presence_of :name
end
I originally chose the above syntax because it was expressive enough to convey my intent in the code. I spent a bit of time making this syntax work, but in the end I went with a syntax that I found a bit drier, and easier to implement.
class FooTest < Test::Unit::TestCase
include ValidatableAssertions

Foo.must_validate do
presence_of :name
format_of(:name).with(/^[A-Z]/)
numericality_of(:age).only_integer(true)
end
end
The above code creates a test for each line in the block given to must_validate. If the Foo class does not contain a presence of validation for name, an error with the text "Foo does not contain a Validatable::ValidatesPresenceOf for name" will be raised.

Clearly this solution has limitations. Any validates_true_for validation cannot be tested using this DSL style of testing. Furthermore, any validation that uses an :if argument cannot use this DSL, since those validations require an instance to eval the :if argument against. However, for validations that are not validates_true_for and do not rely on an :if argument, the ValidatableAssertions can replace various existing success and failure validation tests.

If you hate this idea, no worries, you can always test your classes the traditional way: creating an instance and calling the valid? method. This DSL is another tool that may or may not help you out based on the context in which you use it.

The ValidatableAssertions are available in gem version 1.3.0 and greater.

Sunday, February 11, 2007

Ruby: Validatable

I finished up the 1.1.0 release this morning of Validatable. Validatable is a module that you can mix into your classes to add validations.
class Person
include Validatable
attr_accessor :name
validates_presence_of :name
end

person = Person.new
person.valid? #=> false
person.errors.on(:name) #=> "can't be empty"
Validatable currently supports
  • validates_presence_of
  • validates_length_of
  • validates_format_of
  • validates_confirmation_of
  • validates_acceptance_of
The validations are very similar to the validations that Rails provides. In addition to the traditional Rails functionality, Validatable provides 3 additional features.

Validation of an entire hierarchy of objects with errors aggregated at the root object.
class Person
include Validatable
validates_presence_of :name
attr_accessor :name
end

class PersonPresenter
include Validatable
include_validations_for :person
attr_accessor :person

def initialize(person)
@person = person
end
end

presenter = PersonPresenter.new(Person.new)
presenter.valid? #=> false
presenter.errors.on(:name) #=> "can't be blank"
Validations that turn off after X times of failed attempts.
class Person
include Validatable
validates_presence_of :name, :times => 1
attr_accessor :name
end

person = Person.new
person.valid? #=> false
person.valid? #=> true
Validations can be given levels. If a validation fails on a level the validations for subsequent levels will not be executed.
class Person
include Validatable
validates_presence_of :name, :level => 1, :message => "name message"
validates_presence_of :address, :level => 2
attr_accessor :name, :address
end

person = Person.new
person.valid? #=> false
person.errors.on(:name) #=> "name message"
person.errors.on(:address) #=> nil
Similar to Rails, Validatable also supports conditional validation.
class Person
include Validatable
attr_accessor :name
validates_format_of :name, :with => /.+/, :if => Proc.new { !name.nil? }
end
Person.new.valid? #=> true

Saturday, January 20, 2007

Adding Validations to any Class

On my current project we have data that needs to be collected and then sent to a service. This data needs to be validated, but since it is never put into a model it cannot be validated by using ActiveRecord's validations. To handle this scenario we designed something similar to the module below that can add validations to any class that includes it, which are generally Presenters on my project.
module Validatable
module ClassMethods
def validates_format_of(*args)
validate_all(args) do |attribute, options|
self.validations << ValidatesFormatOf.new(attribute, options[:with], options[:message] || "is invalid")
end
end

def validates_presence_of(*args)
validate_all(args) do |attribute, options|
self.validations << ValidatesPresenceOf.new(attribute, options[:message] || "can't be empty")
end
end

def validate_all(args, &block)
options = args.last.is_a?(Hash) ? args.pop : {}
args.each do |attribute|
yield attribute, options
end
end
protected :validate_all

def validations
@validations ||= []
end

def validate(instance)
self.validations.each do |validation|
instance.errors.add(validation.attribute, validation.message) unless validation.valid?(instance)
end
instance.errors.empty?
end
end

def self.included(klass)
klass.extend Validatable::ClassMethods
end

def valid?
errors.clear
self.class.validate(self)
end

def errors
@errors ||= Validatable::Errors.new
end
end
As you can see I only included the code to expose the validates_presence_of and validates_format_of validations. We actually have quite a bit more, but I think those two are all that's necessary to follow the example. I like the way we designed the validation classes because they can easily be tested and are all treated the same way when it comes time to verify if they are valid or not (the validate method). The code below shows all that is necessary for both the ValidatesPresenceOf and ValidatesFormatOf classes.
module Validatable
class ValidationBase
attr_accessor :message
def initialize(message)
self.message = message
end
end

class ValidatesPresenceOf < ValidationBase
attr_accessor :attribute
def initialize(attribute, message)
self.attribute = attribute
super message
end

def valid?(instance)
(!instance.send(self.attribute).nil? && instance.send(self.attribute).strip.length != 0)
end

end

class ValidatesFormatOf < ValidationBase
attr_accessor :attribute, :regex, :message
def initialize(attribute, regex, message)
self.attribute = attribute
self.regex = regex
super message
end

def valid?(instance)
instance.send(self.attribute) =~ self.regex
end
end
end
The only other piece to the puzzle is the errors collection. The code for this is also very straightforward.
module Validatable
class Errors
extend Forwardable

def_delegators :@errors, :empty?, :clear

def on(attribute)
@errors[attribute.to_sym]
end

def add(attribute, message)
@errors[attribute.to_sym] = message
end

def initialize
@errors = {}
end
end
end
Another important thing to note is that these validations integrate directly with the ActionView::Helpers::ActiveRecordHelper.error_message_on method. This means the same code in the view that displays ActiveRecord errors can also display errors from any object that includes Validatable.
<%= error_message_on :presenter, :name %>
And, here's a few tests if you want to ensure that it works as expected.
class ValidatableTest < Test::Unit::TestCase
test "given no presence when object is validated then valid returns false" do
klass = Class.new
klass.class_eval do
include Validatable
attr_accessor :name
validates_presence_of :name
end

assert_equal false, klass.new.valid?
end

test "given no presence when object is validated then it contains errors" do
klass = Class.new
klass.class_eval do
include Validatable
attr_accessor :name
validates_presence_of :name
end
instance = klass.new
instance.valid?
assert_equal "can't be empty", instance.errors.on(:name)
end

test "given invalid format when object is validated then valid returns false" do
klass = Class.new
klass.class_eval do
include Validatable
attr_accessor :name
validates_format_of :name, :with=>/.+/
end

assert_equal false, klass.new.valid?
end

test "given invalid format when object is validated then it contain errors" do
klass = Class.new
klass.class_eval do
include Validatable
attr_accessor :name
validates_format_of :name, :with=>/.+/
end
instance = klass.new
instance.valid?
assert_equal "is invalid", instance.errors.on(:name)
end

test "given valid data after it is previously invalid when object is validated then it is valid" do
klass = Class.new
klass.class_eval do
include Validatable
attr_accessor :name
validates_format_of :name, :with=>/.+/
end
instance = klass.new
assert_equal false, instance.valid?
instance.name = "Jay"
assert_equal true, instance.valid?
end
end