Quick Tip: DRY Up Your Model Validations Tests
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.