How (to?) Test Validations, Part 4

  • Posted By Stuart Halloway on July 16, 2007

This is Part 4 of the preview for the "Keeping Tests Dry" I will be giving at erubycon tomorrow.

One issue with the previous three code examples is that they are testing ActiveRecord. This is unnecessary: ActiveRecord has its own tests, and I already believe that validations work as advertised.

We can use mocks to simply verify that validations are called with the correct arguments. The only tricky thing is that we are mocking class level behavior that happens when the file is read. To avoid dependencies on other tests, we will read the file again in its own module:

# Gross Trick: reload a model class in a different namespace so we can 
# set expectations without interfering with load of the "real" class
class ValidationInteractionTest < Test::Unit::TestCase
  class ValidationInteractionTest::Contact < ActiveRecord::Base; end

  def test_validates_presence_of_gets_called
    model_source = File.join(RAILS_ROOT, "app/models/contact.rb")
    Contact.expects(:validates_presence_of).with(:name)
    Contact.expects(:validates_presence_of).with(:email)
    self.class.class_eval(File.read(model_source))
  end
end

What do you think? I would be interested to see the Groovy equivalent.

Comments
  1. Jordan McKibleJuly 16, 2007 @ 02:43 PM

    I really don’t like mocks. I think this test is tied too closely to ActiveRecord. What if validates_presence_of changes, you rewrite it another way, or switch to some new plugin version? This test will fail even though Contact could continue to function as you intended.

    I think it’s better to specify the intended behavior of the model rather than test that it’s implemented in a specific way. I’ve never bought into the Mockist philosophy that it helps with design.

  2. Mat SchafferJuly 16, 2007 @ 05:59 PM

    I really like the idea of functional test matrices for this sort of thing. I don’t know if the idea is totally fleshed out just yet though. Some good links for more info are:

    http://drnicwilliams.com/2007/05/22/functional-testing-using-a-matrix-to-cover-all-edge-cases-video/ http://zentest.rubyforge.org/ZenTest/classes/FunctionalTestMatrix.html

  3. Jamie HillJuly 16, 2007 @ 06:53 PM

    I have a test helper plugin I use: http://svn.soniciq.com/public/rails/plugins/iq_test_helper (which requires our validation reflections plugin: http://svn.soniciq.com/public/rails/plugins/iq_validation_reflections)

    This allows me to do: assert_validates_presence_of :name etc… It will also assert that the options are the same such as :max_length

  4. Jamie HillJuly 16, 2007 @ 06:57 PM

    Apologies in advance for the double post. All you actually need in the test helper is the following:

        ActiveRecord::Base.methods.select { |m| m =~ /^validates_.*_of$/ }.map(&:to_sym).each do |macro|
          define_method "assert_#{macro}" do |*args|
            raise 'This assert requires iq_validation_reflections' unless ::ActiveRecord::Base.respond_to?(:reflect_on_validation)
            assert_validation(macro, *args)
          end
        end