Статьи

Совет: СУШИТЕ тестовые испытания вашей модели

Я работал над приложением и должен был протестировать модели, что является довольно обычной практикой. Мне пришлось повторять проверочные тесты для каждого поля и каждой модели, что приводило к большому количеству дублированного тестового кода. Итак, я собираюсь поделиться своим решением этой проблемы, которое поможет нам избежать повторения подобных проверочных тестов для каждой модели.

Я использую Ruby 2.3.0, Rails 4.2.4. И Минитест, и FactoryGirl в моем тестовом наборе. У меня был этот тест, написанный для модели сайта, такой как:

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

В результате чего:

 $ 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

Слишком много кода для простой функции для тестирования, а? Правильно. Представьте, нужно ли вам повторять это во всех моделях для всех полей, которые вы хотите проверить.

Я не знаю, есть ли какие-нибудь драгоценные камни, чтобы высушить эту вещь или нет, так как я не удосужился найти существующие решения этой проблемы. Вместо этого я начал использовать доброту Руби, чтобы найти собственное решение. И вот что я придумал:

 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

Вы можете поместить этот файл в test / support / и запросить все файлы в каталоге поддержки до запуска тестов, добавив эту строку в test / test_helper.rb :

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

Вам просто нужно включить этот модуль в каждый тестовый файл, чтобы использовать эту СУХУЮ версию для проверочных тестов. Кроме того, вы можете пойти дальше и посмотреть этот блок в test / test_helper.rb :

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

Это открывает класс ActiveSupport :: TestCase и расширяет его, это класс, от которого наследуется каждый класс тестирования модели. Добавьте эту строку туда:

 include TestModelValidations

Теперь блок должен выглядеть примерно так:

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

Теперь мы готовы продемонстрировать нашу сухую версию тестов, которые мы видели ранее:

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

Да это оно :). Это все, что нужно для проверки правильности моделей. Теперь у нас есть макросы классов для наших тестовых классов, как и в наших реальных моделях для объявления проверок. Это почти половина кода, который мы ранее должны были написать, чтобы добиться того же самого. Представьте себе использование этого во всех ваших модельных тестах, которые, надеюсь, сэкономят вам сотни строк кода и усилия по копированию / вставке. Давайте снова запустим тест, чтобы убедиться, что все работает так, как было раньше:

 $ 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! Все работает как положено, но гораздо чище и удобнее. Это облегчает жизнь для проверки модели.

Я не знаю, как глубоко я должен объяснить объяснение своей реализации и скольким из вас будет интересно узнать, как эта реализация работает. Так что я собираюсь обернуть это здесь, и если возникнут какие-либо вопросы, я буду рад ответить на них в разделе комментариев или любом другом средстве связи.

Одна вещь, которую я чувствую, выиграет от небольшого объяснения, это макрос / строка:

 test_validates_uniqueness_of FactoryGirl.create(:site), :host

Этот макрос проверяет уникальность модельного поля. Первый аргумент — это постоянный экземпляр модели, из которой будет продублировано поле. После этого первого аргумента вы можете указать любое количество аргументов в качестве полей, по которым вы хотите проверить уникальность.

Вот и все. Ждем ваших комментариев и вопросов. Я действительно надеюсь, что это поможет вам в некотором роде.