神刀安全网

Quick Tip: DRY Up Your Model Validations Tests

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 DRY ed 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 DRY ed 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.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Quick Tip: DRY Up Your Model Validations Tests

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址