Rails 5.2 rspec - 如何测试模型是否真的在使用自定义验证器?

Rails 5.2 rspec - How to test if a model is actually using a custom validator?

我创建了一个自定义验证器,它有自己的特定单元测试来检查它是否有效。

在那里使用应该匹配器 was a suggestion 添加一个 validates_with 匹配器,所以你可以写:

subject.validates_with(:custom_validator)

这个建议被拒绝是正确的,因为它并没有真正测试模型的行为。

但我的模型有 4 个使用自定义验证器的字段,我希望测试该行为 - 即正在验证这 4 个字段,就像我正在测试它们是否存在一样:

describe '#attribute_name' do
  it { is_expected.to validate_presence_of(:attribute_name) }
end

那么我怎样才能编写一个基本上做同样事情的测试,有点像这样:

describe '#attribute_name' do
  it { is_expected.to use_custom_validator_on(:attribute_name) }
end

This question 问同样的事情,答案建议建立一个测试模型。但是,我的验证器需要一个选项,它是这样使用的:

\app\models\fund.rb

class Fund < ActiveRecord
  validates :ein, digits: { exactly: 9 }
end

因此,如果我构建一个测试模型,并按照建议对其进行测试:

it 'is has correct number of digits' do
  expect(build(:fund, ein: '123456789')).to be_valid
end

it 'is has incorrect number of digits' do
  expect(build(:fund, ein: '123').to be_invalid
end

我收到 RecordInvalid 错误(来自我自己的验证器!大声笑)说我没有为验证器提供所需的选项。该选项称为 'exactly'.

1) Fund#ein validates digits
     Failure/Error: raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)

     ActiveRecord::RecordInvalid:
       Record invalid

所以 Rspec 不是 'seeing' 模型文件中定义的值 '9' 吗?[​​=26=]

显然,在测试中定义它是没有意义的,因为这是我要测试的定义行为。可以把它想象成 validates_length_of 测试 { length: x } 选项。

肯定有一种方法可以测试该自定义验证器选项是否已在模型上设置?

验证者代码

class DigitsValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if value.blank?

    length = options[:exactly]
    regex = /\A(?!0{#{length}})\d{#{length}}\z/
    return unless value.scan(regex).empty?

    record.errors[attribute] << (options[:message] || error_msg(length))
  end

  private

  def error_msg(length)
    I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length) if length
    raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
  end
end

有趣的边注

显然,如果我从 DigitsValidator 中删除 'raise' 行,那么两个测试都会成功。我的代码有什么我看不到的问题吗?

我认为您不应该以测试模型是否使用特定验证器为目标。而是在特定情况下检查模型是否为 valid/invalid。换句话说,您应该能够在不知道实现的情况下测试模型的行为。

因此,在这种情况下,您应该使用验证器的 'exactly' 选项正确设置您的模型,并测试模型验证总体上是否足够。

另一方面,如果您担心将来有人会滥用验证器并且 'exactly' 是验证器的必需选项,那么您应该在每次没有该选项时抛出错误像这里解释的那样孤立地呈现和测试验证器:How to test a custom validator?

我认为您必须添加一个 return 语句,不是吗? :-)

 def error_msg(length)
   return I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length) if length
   raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
 end

或者,在设置 length:

后删除该方法并使用保护
  class DigitsValidator < ActiveModel::EachValidator
    def validate_each(record, attribute, value)
      return if value.blank?

      length = options[:exactly]
      raise ActiveRecord::RecordInvalid if length.nil?

      regex = /\A(?!0{#{length}})\d{#{length}}\z/
      return unless value.scan(regex).empty?

      record.errors[attribute] << 
        (options[:message] || 
          I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length))
      end
    end

我喜欢不包括模型测试的想法,这些测试假设知道自定义验证器正在验证的确切内容。 (否则,我们将在自定义验证器的测试和模型的测试中重复逻辑。)

我通过使用 Mocha(Ruby 的模拟库)来设置预期,即在正确的对应字段上调用我的每个自定义验证器的 validate_each 方法来解决这个问题我的模型。简化示例:

型号class:

class User
    include ActiveModel::Model

    attr_accessor :first_name, :last_name

    validates :first_name, first_name: true
    validates :last_name, last_name: true
end

自定义验证器 classes:

class FirstNameValidator < ActiveModel::EachValidator
    def validate_each(record, attribute, value)
        # ...
    end
end

class LastNameValidator < ActiveModel::EachValidator
    def validate_each(record, attribute, value)
        # ...
    end
end

模型测试class:

class UserTest < ActiveSupport::TestCase
    def test_custom_validators_called_on_the_appropriate_fields
        user = User.new(first_name: 'Valued', last_name: 'Customer')

        FirstNameValidator.any_instance.expects(:validate_each).with(user, :first_name, 'Valued')
        LastNameValidator.any_instance.expects(:validate_each).with(user, :last_name, 'Customer')

        assert_predicate user, :valid?
    end
end