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.
Frequently Asked Questions (FAQs) about DRYing Up Your Model Validation Tests
What is the DRY principle in coding?
The DRY principle stands for “Don’t Repeat Yourself”. It is a software development principle aimed at reducing repetition of software patterns. Instead of writing the same code multiple times, the DRY principle encourages the use of methods that can be used in multiple places. This makes the code more efficient, easier to maintain, and less prone to errors.
How does the DRY principle apply to model validation tests?
In model validation tests, the DRY principle can be applied by creating reusable test cases. Instead of writing a new test for each validation rule, you can create a method that tests a given rule and then call that method for each rule. This reduces the amount of code you need to write and makes your tests easier to read and maintain.
What is FactoryGirl and how is it used in model validation tests?
FactoryGirl is a Ruby library for setting up Ruby objects as test data. It allows you to define a blueprint for each type of object, and then create instances of that object with different attributes for each test. In model validation tests, FactoryGirl can be used to create valid instances of your model, which you can then modify to test different validation rules.
How can I use FactoryGirl to DRY up my model validation tests?
You can use FactoryGirl to create a valid instance of your model, and then modify that instance for each test. For example, if you have a validation rule that requires a user’s email to be unique, you can create a user with FactoryGirl, then try to create another user with the same email and check that the validation fails. This allows you to test multiple validation rules with a single instance, reducing the amount of code you need to write.
What is RSpec and how is it used in model validation tests?
RSpec is a testing tool for Ruby. It provides a DSL (Domain Specific Language) for writing tests in a way that’s easy to read and understand. In model validation tests, RSpec can be used to define your tests and assertions. It also integrates well with FactoryGirl, allowing you to easily set up test data for your tests.
How can I use RSpec to DRY up my model validation tests?
You can use RSpec’s shared examples feature to DRY up your model validation tests. Shared examples allow you to define a group of tests that can be reused with different parameters. For example, you can define a shared example that tests a validation rule, and then include that example in your model tests with different parameters for each rule.
What are some best practices for DRYing up model validation tests?
Some best practices for DRYing up model validation tests include using FactoryGirl to set up test data, using RSpec’s shared examples to reuse test cases, and organizing your tests in a way that’s easy to read and understand. It’s also important to make sure your tests are thorough and cover all possible scenarios.
What are some common mistakes to avoid when DRYing up model validation tests?
Some common mistakes to avoid when DRYing up model validation tests include overcomplicating your tests, not covering all possible scenarios, and not maintaining your tests as your codebase evolves. It’s also important to avoid relying too heavily on FactoryGirl for setting up test data, as this can make your tests slower and harder to understand.
How can I ensure my model validation tests are thorough?
To ensure your model validation tests are thorough, you should test all possible scenarios for each validation rule. This includes both positive cases (where the validation should pass) and negative cases (where the validation should fail). You should also test edge cases, such as the minimum and maximum values for a field.
How can I maintain my model validation tests as my codebase evolves?
To maintain your model validation tests as your codebase evolves, you should regularly review and update your tests to reflect changes in your code. This includes adding new tests for new validation rules, and updating existing tests if the behavior of a rule changes. It’s also important to keep your tests organized and easy to read, so you can quickly identify and fix any issues.
Quick Tip: DRY Up Your Model Validations Tests