Ruby
Article

Quick Tip: DRY Up Your Model Validations Tests

By Sarmad Sabih

I was working on an application and needed to test the models, which is a pretty normal practice. I had to repeat validation tests for each field and each model resulting in lots of duplicated test code. So, I’m going to share my solution to this problem, which will help us avoid repeating similar validation tests for each model.

I’m using Ruby 2.3.0, Rails 4.2.4. And Minitest and FactoryGirl in my test suite. I had this test written for a Site Model like:

class SiteTest < ActiveSupport::TestCase
  def test_should_require_customer_name
    site = Site.new
    refute site.valid?
    refute site.save
    assert_operator site.errors.count, :>, 0
    assert site.errors.messages.include?(:customer_name)
    assert site.errors.messages[:customer_name].include?("can't be blank")
  end

  def test_should_require_customer_email
    site = Site.new
    refute site.valid?
    refute site.save
    assert_operator site.errors.count, :>, 0
    assert site.errors.messages.include?(:customer_email)
    assert site.errors.messages[:customer_email].include?("can't be blank")
  end

  def test_should_require_host
    site = Site.new
    refute site.valid?
    refute site.save
    assert_operator site.errors.count, :>, 0
    assert site.errors.messages.include?(:host)
    assert site.errors.messages[:host].include?("can't be blank")
  end

  def test_should_require_host_to_be_unique
    theme = FactoryGirl.create(:theme)
    Site.skip_callback(:create, :after, :setup_components)
    existing_site = FactoryGirl.create(:site, theme: theme)
    Site.after_create(:setup_components)
    site = Site.new(host: existing_site.host)
    refute site.valid?
    refute site.save
    assert_operator site.errors.count, :>, 0
    assert site.errors.messages.include?(:host)
    assert site.errors.messages[:host].include?("has already been taken")
  end

  def test_should_require_theme
    site = Site.new
    refute site.valid?
    refute site.save
    assert_operator site.errors.count, :>, 0
    assert site.errors.messages.include?(:theme)
    assert site.errors.messages[:theme].include?("can't be blank")
  end

  def test_should_require_user
    site = Site.new
    refute site.valid?
    refute site.save
    assert_operator site.errors.count, :>, 0
    assert site.errors.messages.include?(:user)
    assert site.errors.messages[:user].include?("can't be blank")
  end

end

Which resulted in:

$ ruby -Ilib:test test/models/site_test.rb
SiteTest
 test_should_require_user PASS (0.45s)
 test_should_require_host PASS (0.01s)
 test_should_require_customer_email PASS (0.01s)
 test_should_require_host_to_be_unique PASS (0.09s)
 test_should_require_theme PASS (0.01s)
 test_should_require_customer_name PASS (0.01s)
Finished in 0.58104s
6 tests, 30 assertions, 0 failures, 0 errors, 0 skips

Too much code for a simple feature to test, eh? Right. Imagine if you have to repeat it in all models for all the fields you want to validation.

I don’t know if there are any Gems to DRY this thing out or not, as I didn’t bother to search for existing solutions to this problem. Instead, I started using the Ruby’s OO goodness to carve out a light solution of my own. And this is what I came up with:

module TestModelValidations
  def self.included(klass)
    klass.class_eval do

      def self.test_validates_presence_of(*args)
        args.each do |field_name|
          define_method("test_should_require_#{field_name.to_s}") do
            model = self.class.model_klass.new
            assert_validation(model, field_name, "can't be blank")
          end
        end
      end

      def self.test_validates_uniqueness_of(existing_model, *args)
        args.each do |field_name|
          define_method("test_should_require_#{field_name.to_s}_to_be_unique") do
            params_hash = {}
            params_hash[field_name] = existing_model.send(field_name)
            model = self.class.model_klass.new(params_hash)
            assert_validation(model, field_name, "has already been taken")
          end
        end
      end

    private
      def assert_validation(model, field_name, error_message)
        refute model.valid?
        refute model.save
        assert_operator model.errors.count, :>, 0
        assert model.errors.messages.include?(field_name)
        assert model.errors.messages[field_name].include?(error_message)
      end

    end
  end

  def model_klass
    self.class.name.underscore.split("_test").first.camelize.constantize
  end
end

You can place this file in test/support/ and require all files in support directory before the tests are started by adding this line in test/test_helper.rb :

Dir[Rails.root.join(‘test’, ‘support’, ‘*.rb’)].each { |f| require f }

You just need to include this module in every test file to use this DRYed up version of the validation tests. Also, you can go a step further and look for this block in test/test_helper.rb :

class ActiveSupport::TestCase
 ActiveRecord::Migration.check_pending!
end

Which opens the ActiveSupport::TestCase class and extends it, this is the class which every Model test class inherits from. Add this line in there:

include TestModelValidations

Now the block should look something like this:

class ActiveSupport::TestCase
 ActiveRecord::Migration.check_pending!
 include TestModelValidations
end

Now we’re ready to demonstrate our DRYed up version of the tests we saw earlier:

class SiteTest < ActiveSupport::TestCase
  test_validates_presence_of :customer_email, :customer_name, :host, :theme, :user
  test_validates_uniqueness_of FactoryGirl.create(:site), :host
end

Yes, that’s it :). That’s all it takes to test model validations. Now we have class macros for our test classes just like we have in our actual models for declaring validations. That’s almost half the code we previously had to write to achieve the same thing. Imagine using this in all your model tests, which hopefully will save you hundreds of lines of code and copy/paste effort. Let’s run the test again to ensure that everything is working as it was before:

$ ruby -Ilib:test test/models/site_test.rb
SiteTest
 test_should_require_customer_name PASS (0.34s)
 test_should_require_user PASS (0.01s)
 test_should_require_host PASS (0.01s)
 test_should_require_host_to_be_unique PASS (0.01s)
 test_should_require_theme PASS (0.01s)
 test_should_require_customer_email PASS (0.01s)
Finished in 0.39483s
6 tests, 30 assertions, 0 failures, 0 errors, 0 skips

Boom! Everything works as expected, but is much cleaner and convenient. This makes life easier for model validation testing.

I don’t know how deep I should go into explaining my implementation and how many of you will be interested to know how this implementation works. So I’m just gonna wrap this up here and if any questions arise, I’ll be glad to answer them in the comments section or any other communication medium.

One thing I feel would benefit from a bit of explanation is this macro/line:

test_validates_uniqueness_of FactoryGirl.create(:site), :host

This macro validates the uniqueness of a model field. The first argument is the persisted instance of the model from which the field will be duplicated. After that first argument, you can provide any number of arguments as fields on which you want to validate uniqueness.

That’s it. Looking forward to your comments and questions. I really hope this helps you in some way.

  • Hassanin Ahmed

    Something’s up with the syntax highlighting.

    • ggsp

      How so?

      • Hassanin Ahmed

        Either it’s been fixed or it didn’t load properly for me the last time. Looks good now, thanks.

  • Serguei Cambour

    Great solution, Sarmad, thank you for sharing. I’ve seen a lot of similar duplicated validation tests and now I know how to solve that :)

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.