【问题标题】:Testing Rails model validations with RSpec, without testing AR itself使用 RSpec 测试 Rails 模型验证,无需测试 AR 本身
【发布时间】:2016-05-21 16:08:24
【问题描述】:

使用 RSpec 测试 Rails 模型验证,而不测试 AR 本身

让我们设置模型User

class User < ActiveRecord::Base
  validate :name, presence: true, uniqueness: { case_sensitive: false }, on: :create
  validate :password, presence: true, format: { with: /\A[a-zA-z]*\z/ }
end

查看几种测试方法:

it { expect(user).to validate_presence_of(:name).on(:create) } 

it do
  user = User.create(name: '')

  expect(user.errors[:name]).to be_present
end

我的主要问题是哪种方法更好,为什么?可以建议我不同的方法吗?

其他问题:

  • 我应该测试多少?例如,我可以为正则表达式编写这么多测试,但维护起来会很麻烦。
  • 您认为此示例中的完整测试覆盖率有多少?

【问题讨论】:

  • 仅供参考,validate_presence_of 匹配器不是 RSpec 内置的,它是由 shoulda-matchers gem 添加的:github.com/thoughtbot/shoulda-matchers
  • 第一种更紧凑更好,它暗示了第二种方法的功能

标签: ruby-on-rails ruby unit-testing testing rspec


【解决方案1】:

以下功能:

  • Rails 能够验证模型上是否存在任意值
  • errors 被添加到对象中,以获取在配置验证时缺少的属性

包含在 Rails 本身的测试中(特别是在 ActiveModel tests 中)。

这就需要为 config 编写测试,涵盖 your 应用的业务逻辑,例如验证 specific @ 特定 User 类上的 987654325@ 属性等。在我看来,shoulda-matchers gem 的匹配器应该可以覆盖您:

RSpec.describe User, type: :model do
  subject(:user) { build(:user) } # assuming you're using FactoryGirl

  describe 'validations' do
    specify 'for name' do
      expect(user).to validate_presence_of(:name).on(:create)
      # NOTE: saving here is needed to test uniqueness amongst users in
      # the database
      user.save
      expect(user).to validate_uniqueness_of(:name)
    end

    specify 'for password' do
      expect(user).to validate_presence_of(:password)
      expect(user).to allow_value('abcd').for(:password)
      expect(user).to_not allow_value('1234').for(:password)
    end
  end
end

我认为,除非您有特定的自定义错误消息用于您想要测试的错误(即您已经覆盖了默认的 Rails 错误消息),否则可以删除像 expect(user.errors[:name]).to be_present 这样的测试(即使您有自定义错误) ,我仍然认为它们的价值值得怀疑,因为如果您将应用程序国际化,这些消息将变得依赖于语言环境,所以我会在功能规范中测试 page 上显示的某种错误。

我可以为正则表达式编写这么多测试,但维护起来会很麻烦。

我认为您在测试 format 的验证时无法真正解决这个问题,因此我建议您只编写一些具有代表性的测试用例,然后在您发现可能遗漏的任何问题时添加/删除这些用例,例如:

# use a `let` or extract out into a test helper method
let(:valid_passwords) do
  ['abcd', 'ABCD', 'AbCd'] # etc etc
end

describe 'validations' do
  specify 'for password' do
    valid_passwords.each do |password|
      expect(user).to allow_value(password).for(:password)
    end
  end
end

您认为本示例中的完整测试覆盖率有多少?

在编写如上所述的单元规范时,我从 SimpleCov 之类的报告中获得了 100% 的代码覆盖率。

【讨论】:

    【解决方案2】:

    应该使用这两个,因为:

    it { expect(user).to validate_presence_of(:name).on(:create) } 
    

    =>您期望validate_presence_of 应该在create 上运行,这应该是模型的测试用例

    it do
      user = User.create(name: '')
      expect(user.errors[:name]).to be_present
    end
    

    => 使用您的输入创建用户时,您预计会有副作用,因此这应该是控制器的测试用例

    为什么不应该删除其中的 1 个:

    • 删除第一个测试用例:如果您改为执行数据库验证级别会发生什么,您希望进行活动记录级别验证

    • 删除第二个测试用例:控制器上发生的事情实际上创建了一个新的User,您如何期望错误返回!

    【讨论】:

    • 所以你建议第二个测试用例是控制器单元测试?所以你更喜欢第一个进行模型单元测试?
    • 是的,这就是我正在考虑并在我当前的项目中使用的,我们通常测试行为(调用什么)和测试副作用(发生什么)!
    猜你喜欢
    • 2011-11-24
    • 1970-01-01
    • 2017-05-28
    • 2010-09-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-09
    • 1970-01-01
    相关资源
    最近更新 更多