Picture of stu

How Not to Test Validations, Part 2

  • Posted By Stuart Halloway on July 14, 2007

This is Part 2 of the preview for "Keeping Tests Dry" at next week's erubycon.

Several people offered improvements to the bad code from Part 1. Here is my own second cut, much better than the first:

  # Still tests ActiveRecord as well as your code, but at least
  # no exception handling
  # tests only one attribute
  # not (as) fragile
  def test_name_validation
    c = Contact.new
    assert(!c.valid?)
    assert_equal("can't be blank", c.errors['name'])
  end

This could still be a lot better. How would you DRY this up for use across a large codebase? (More to follow...)

Comments
  1. meekishJuly 14, 2007 @ 05:18 PM

    ‘assert(!c.valid?)’ is a superfluous assertion. Simply call ‘c.valid?’ to add the errors to the model.

    Also, it would be less brittle to simply assert the presence of an error on c.name:

    assert_not_nil(c.errors_on(:name))

    Admittedly, this assertion would still pass if the errors were caused by other validations on c.name (i.e. minimum length, letters only, etc.), but I think it’s a step in the right direction.

  2. Sammy LarbiJuly 14, 2007 @ 07:20 PM

    How about simply asserting the presence of the name key in the errors hash (and that it isn’t blank)?

  3. Luke RedpathJuly 14, 2007 @ 07:45 PM

    Using RSpec, and a custom matcher:

    c = Contact.new c.should be_invalid_without_a(:name)
  4. Luke RedpathJuly 14, 2007 @ 07:47 PM

    Using RSpec, and a custom matcher:

    c = Contact.new c.should be_invalid_without_a(:name)
  5. Gavin StarkJuly 14, 2007 @ 08:15 PM

    I’ve been experimenting with setting my validations like:

    VALIDATION_ERRORS = {
      :no_name                    => "you must supply a name",
      # Other errors would go here
     }

    So I could write:

    validates_presence_of :name, :message => VALIDATION_ERRORS[ :no_name ]

    and in the above you could write: assert_equal(Contact::VALIDATION_ERRORS[:no_name], c.errors[‘name’])

    Also the code above makes assertions that Contact doesn’t set a default name, perhaps: c = Contact.new( :name => nil ) would be more explicit? Perhaps also, in a separate test, try c = Contact.new( :name => ”” ) ?

  6. Dylan StamatJuly 15, 2007 @ 02:28 AM

    +1 on what Luke said.

    If you weren’t using a custom matcher (however, you really should one), you could just load up the Contact instance with valid attributes, minus the name, and assert the validity of that.

  7. sjsJuly 16, 2007 @ 05:56 AM

    I normally use test/spec, maybe I should switch to rspec. Anyway you could build a valid contact or grab a fixture, I chose the fixture route below.

    RequiredAttrs = {
      :name => :blank,
      :email => [ :blank, :taken ]
    }
    
    RequiredAttrs.each do |attr_name, error|
      define_method("test_#{attr_name}_validation") do
        c = contacts(:joe)
        orig = c.send(attr_name)
        attr_assign = :"#{attr_name}=" 
        c.send(attr_assign, nil)
        assert !c.valid?
        case error
        when String
          assert_equal c.errors.on(attr_name), ActiveRecord::Errors.default_error_messages[error]
        when Enumerable
          errors = error.map {|e| ActiveRecord::Errors.default_error_messages[e] }.sort
          assert_equal c.errors.on(attr_name).sort, errors
        end
        c.send(attr_assign, orig)
        assert c.valid?
      end
    end