Testing Validation the DRY Way

UPDATE: Please have a look at valid_attributes

We all know the discussion: should we test simple validation or not ? Here is a great argument for testing even the most simple validation logic.

assert_invalid_attributes User, :login=>[”,nil,’admin’], :email=>[”,nil,’aa’]

Can it get any simpler ?

It goes through all of them and and tests them one by one, giving hepful error messages like:

<User.login> expected to be invalid when set to <admin>

All you need to do is define a @valid_attributes = {:username=>’foo’…} hash in your test setup to test things like ‘password is not username’ and never worry about validations again!

This goes to test/test_helper.rb or spec/spec_helper.rb

  #create a set of invalid attributes
def invalid_attributes search='', replace=''
  @valid_attributes ||= {}
  @valid_attributes[search]=replace unless search.blank?
  @valid_attributes
end

#idea: http://www.railsforum.com/viewtopic.php?id=741
#example: User, :login=>['',nil,'admin'], :email=>['',nil,'aa','@','a@','@a']
def assert_invalid_attributes(model_class, attributes)
  attributes.each_pair do |attribute, value|
    assert_invalid_value model_class, attribute, value
  end
end

#idea: http://www.railsforum.com/viewtopic.php?id=741
#example: User, :login, ['',nil,'admin']
def assert_invalid_value(model_class, attribute, value)
  if value.kind_of? Array
     value.each { |v| assert_invalid_value model_class, attribute, v }
  else
    attributes = invalid_attributes(attribute,value)
    record = model_class.new(attributes)
    assert_block "<#{model_class}.#{attribute}> expected to be invalid when set to <#{value}>" do
      record.valid? # Must be called to generate the errors
      record.errors.invalid? attribute
    end
  end
end

Convert Test::Unit to RSpec by Script

Living with Test and Spec at the same time is annoying, so here is a small Howto for conversion using the Test::Unit to RSpec converter.

  1. Change spec/spec_helper.rg ‘config.fixture_path =‘ to test/fixtures OR copy all fixtures from test to spec (svn cp test/fixtures spec/fixtures)
  2. copy old code from test_helper to spec_helper (leave includes outside of Spec::Runner.configure do…)
  3. sudo gem install spec_converter
  4. convert all tests ‘spec_converter
  5. correct syntax errors
  6. search and replace ‘test/xxx’ with ‘spec/xxx’ where neccessary
  7. run ‘rake spec
  8. dance if result == :success

Works very nice for me, just one syntax error to correct, and then everything runs. Not all old asserts will be replaced, but this is not problem, since RSpec can work with Test::Unit assertions.

Instant Bug to Testcase

Today i found a small new plugin, that offers a simple way to convert bugs to a testcase.

script/plugin install http://svn.extendviget.com/lab/laziness/trunk

Now every failing request will print a small test case to repeat this request, it is not much, but it is a starting point and can be helpful if you have a lot of parameters being passed.

Output:
Laziness
def test_get_rating_edit_should_not_raise_activerecord_recordnotfound_exception
  assert_nothing_raised(ActiveRecord::RecordNotFound) do
    get :edit, {"id"=>"1"}, {:user_id=>nil, :return_to=>"/orders/1"}, {}, {"_session_id"=>["…"]}
  end
end
Patch:

Atm you need to apply this patch to make it work without exception_notification.

#vendor/plugins/laziness/lib/laziness.rb
module Laziness
  begin
    module ExceptionNotifier
      ::ExceptionNotifier.sections << 'laziness'
    end
  rescue
  end

Validate all Fixtures

UPDATE: Please have a look at valid_attributes

A enhancement to the validate all models task , adds a db:validate_fixtures, which basically loads the fixtures and then validates all models.


To make this work for RSpec you have to “ln -s PATH_TO/spec/fixtures test/fixtures”.

Output:

-- records - model --
         1x FtpAccount
         2x Movie
Movie: id=2
["Title can't be blank"]
         1x Order
         1x Rating
No Table for: Tableless
         4x User

Task:

#base: http://blog.hasmanythrough.com/2006/8/27/validate-all-your-records
#task: rake db:validate_models
namespace :db do
  def all_models
    #get all active record classes
    Dir.glob(RAILS_ROOT + '/app/models/**/*.rb').each do |file|
      begin
        require file unless file =~ /observer/
      rescue
        #require any possible file dependencies
        if $!.to_s =~ /^uninitialized constant (\w+)$/
          require $1.underscore + '.rb'
          retry
        else
          raise
        end
      end
    end
    klasses = Object.subclasses_of(ActiveRecord::Base)
    #throw out session if it is not stored in db
    klasses.reject! do |klass|
      klass == CGI::Session::ActiveRecordStore::Session && ActionController::Base.session_store.to_s !~ /ActiveRecordStore/
    end
    klasses.select{ |c| c.base_class == c}.sort_by(&:name)
  end

  def validate_models
    #validate them
    puts "-- records - model --"
    all_models.each do |klass|
      begin
        total = klass.count
      rescue
        #tableless, session...
        if $!.to_s =~ /Table .* doesn't exist/im
          puts "No Table for: #{klass}"
          next
        end
        raise
      end
      printf "%10dx %s\n", total, klass.name
      chunk_size = 1000
      (total / chunk_size + 1).times do |i|
        chunk = klass.find(:all, :offset => (i * chunk_size), :limit => chunk_size)
        chunk.reject(&:valid?).each do |record|
          puts "#{record.class}: id=#{record.id}"
          p record.errors.full_messages
          puts
        end rescue nil
      end
    end
  end

  desc "Run model validations on all model records in database"
  task :validate_models => :environment do
    validate_models
  end

  desc "Load and validate fixtures (and other models)"
  task :validate_fixtures do
    RAILS_ENV='test'
    
    Rake::Task[:environment].invoke
    Rake::Task["db:fixtures:load"].invoke
    
    validate_models
  end
end

Testing a single Example; Spec; Testcase; Test

UPDATE: everything is now on github

Testing a single spec, a single test, a single testcase or a single example is a great timesaver when debugging a small problem while having a large testsuite, or a testsuite with a lot of failures…

  • rake test:blog -> only the Blog Testcase
  • rake spec:blog -> only the Blog Spec
  • rake test:blog:create -> only the tests matching /create/ in Blog
  • rake spec:blog:delete -> only the first example matching /create/ in Blog
  • rake test:’admin/blogs_con’ -> only BlogsController Test in admin folder
  • rake test:xy -> first test matching xy*_test.rb (searched in order: unit,functional,integration,any folder)

It will search in unit/functional/integration (test) and models/controllers/views/helpers(spec).
Idea

Install:
UPDATE: everything is now on github