如何测试哪个验证在ActiveRecord中失败?

29

我有一个这样的模型:

class User < ActiveRecord::Base
  validates_length_of :name, :in => (2..5)
end

我想测试这个验证:

it "should not allow too short name" do
  u = User.new(:name => "a")
  u.valid?
  u.should have(1).error_on(:name)
end

但是它并没有测试在name上设置了哪种类型的错误。 我想知道是否为too_shorttoo_long或者其他一些验证失败。

我可以像这样在错误数组中查找消息文本:

u.errors[:name].should include(I18n.t("activerecord.errors.models.user.attributes.name.too_short"))

但是,当我在语言文件中设置 activerecord.errors.messages.too_short 而不是特定于模型的消息时,这将失败。

那么,有可能检查发生了哪种错误吗?

4个回答

43

Rails 在 2011 年后期和 Rails v3.2 中为 ActiveModel 添加了一种查询错误的方法。只需检查是否已经#added?相应的错误:

# An error added manually
record.errors.add :name, :blank
record.errors.added? :name, :blank # => true

# An error added after validation
record.email = 'taken@email.com'
record.valid? # => false
record.errors.added? :email, :taken, value: 'taken@email.com' # => true

请注意,对于需要参数的验证(例如:greater_than_or_equal_to),您还需要传递参数的值。
record.errors.add(:age, :greater_than_or_equal_to, count: 1)
record.errors.added?(:age, :greater_than_or_equal_to, count: 1)

错误由其i18n键标识。您可以在错误部分下的任何语言的适当Rails i18n文件中找到相应的键以进行检查。
您可以询问 ActiveModel#Error 的其他巧妙问题,例如#empty?#include?(attr),以及您可以询问任何 Enumerable 的内容。

1
added? 是一个方便的方法。结合 rspec 语法,可以使其读起来非常流畅。例如:expect(record.errors).to be_added :name, :blank - PhilT
3
请注意,在某些检查中,您需要将选项作为第三个参数传递。例如,assert record.errors.added?(:slug, :too_short, count: 5),其中5是所需的长度。 - Gerry Shaw
请注意,此解决方案仅适用于模型验证。无法使用“#added?”检查任意错误。例如,在上面的示例中,如果检查“:name”或“:email”的错误是否为模型属性,“#added?”将返回一个错误。 - sealocal
不幸的是,added? 在内部仍然执行字符串比较错误消息。这意味着我们必须传入插值值才能使其工作: https://github.com/rails/rails/pull/14067/files#r9781601,这有点让人烦恼。 - lulalala

12

我真的不喜欢在Errors哈希中寻找翻译后的错误信息这个想法。在与一位同样使用Ruby的人交谈后,我最终对Errors哈希进行了Monkey Patching,这样它可以先保存非翻译的信息。

module ActiveModel
  class Errors
    def error_names
      @_error_names ||= { }
    end

    def add_with_save_names(attribute, message = nil, options = {})
      message ||= :invalid
      if message.is_a?(Proc)
        message = message.call
      end
      error_names[attribute] ||= []
      error_names[attribute] << message
      add_without_save_names(attribute, message, options)
    end

    alias_method_chain :add, :save_names
  end
end

然后你可以这样进行测试:

u = User.new(:name => "a")
u.valid?
u.errors.error_names[:name].should include(:too_short)

3
如果 Rails 有验证类型,那它应该完全做到这一点...但事实上,验证类型没有保存到错误对象中,这很奇怪且令人沮丧。 - Mike Campbell
3
@MikeCampbell 这个答案有点过时了。Rails v3.2 通过 Error#added? 来提供这种能力。有关更多信息,请参阅我的回答 - fny
@faraz,太棒了。我简直不敢相信我找不到它。谢谢! - Mike Campbell
我会避免任何猴子补丁。 Rails 5:person = Person.new person.validate person.errors.details[:name] # => [{error: :blank}]Rails 4 回溯:https://github.com/cowbell/active_model-errors_details - Artur INTECH

5

我建议使用宝石库shoulda来处理这些重复性的验证测试。它与RSpec或Test::Unit相辅相成,使您可以编写简洁的规范,例如:

describe User do
  it { should ensure_length_of(:name).is_at_least(2).is_at_most(5) }
end

仅从代码上看,它似乎只是在查找消息翻译。看起来很不错,至少这样代码作为专用库的一部分得到了维护,而不是作为应用程序的一部分。 - Paul Russell
感谢您的回答。我的目标是测试更复杂的验证场景,所以我不想使用 shoulda。我想如果 shoulda 是这样工作的话,就没有其他方法可以检查哪个验证失败了。 - Jan Dudek

5
我使用的方法是:

it "should not allow too short name" do
  u = User.new(:name => "a")
  expect{u.save!}.to raise_exception(/Name is too short/)
end

我使用正则表达式来匹配异常消息,因为异常消息中可能有许多验证消息,但我们希望确保它包含与名称太短相关的特定片段。

这种方法将你的断言与验证消息耦合在一起,因此如果您修改验证消息,您很可能也需要修改您的规范。总的来说,这是一种简单的方法来断言验证是否发挥作用。


但是如果测试失败,这将保存记录。其他副作用也可能发生。为什么不使用“valid”?以确保您只测试验证? - PhilT

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接