Rails Rspec - 如何设置多态has_many关联

Rails Rspec - How to set polymorphic has_many association

我有一个模型(付款)通过多态关联属于另一个模型(事件)。

一些测试失败,因为所有者模型(事件)在验证中被支付模型访问,但事件返回 nil。直接在浏览器中测试应用程序时,所有功能都可以正常工作。 我在下面的 payment.rb 中添加了一些评论。

我试过在工厂中定义关联,但没有成功。

在规范中设置此关联的最佳方法是什么?

# models/event.rb

class Event < ApplicationRecord

  has_many :payments, as: :payable, dependent: :destroy

end
# models/payment.rb

class Payment < ApplicationRecord

  belongs_to :payable, polymorphic: true

  validate :amount_is_valid

  def amount_is_valid

    if amount.to_i > payable.balance.to_i
      errors.add(:amount, "can't be higher than balance")
    end

  end
  
end

本规范中的两个示例均失败。

# spec/models/payment_spec.rb

require 'rails_helper'

RSpec.describe Payment, type: :model do
  
  let!(:event) { FactoryBot.create(:event, event_type: 'test', total: 10000, balance: 10000) }
  let!(:user) {FactoryBot.create(:user)}
  let!(:payment) { 
    FactoryBot.build(:payment, 
      amount: 300,
      method: 'cash', 
      payer_id: user.id,
      payable_id: event.id, 
      status: 1,
    )
  }

  describe 'Association' do
    it do 
      
      # This will fail with or without this line 
      payment.payable = event

      is_expected.to belong_to(:payable)
    end

  end

  # Validation
  describe 'Validation' do

    describe '#amount_is_valid' do 
      it 'not charge more than event balance' do 

        # This will make the test pass. The actual spec has a lot more examples though,
        # would rather just set the association once.
        
        # payment.payable = event 

        payment.amount = 5000000
        payment.validate 
        expect(payment.errors[:amount]).to include("can't be higher than balance")
      end
    end
 
  end 
end

输出


# bundle exec rspec spec/models/payment_spec.rb

Randomized with seed 42748

Payment
  Association
    should belong to payable required: true (FAILED - 1)
  Validation
    #amount_is_valid
      not charge more than event balance (FAILED - 2)

Failures:

  1) Payment Association should belong to payable required: true
     Failure/Error: if amount.to_i > payable.balance.to_i
     
     NoMethodError:
       undefined method `balance' for nil:NilClass
     # ./app/models/payment.rb:9:in `amount_is_valid'
     # ./spec/models/payment_spec.rb:23:in `block (3 levels) in <top (required)>'
     # ./spec/rails_helper.rb:80:in `block (3 levels) in <top (required)>'
     # ./spec/rails_helper.rb:79:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:108:in `block (2 levels) in <top (required)>'

  2) Payment Validation #amount_is_valid not charge more than event balance
     Failure/Error: if amount.to_i > payable.balance.to_i
     
     NoMethodError:
       undefined method `balance' for nil:NilClass
     # ./app/models/payment.rb:9:in `amount_is_valid'
     # ./spec/models/payment_spec.rb:39:in `block (4 levels) in <top (required)>'
     # ./spec/rails_helper.rb:80:in `block (3 levels) in <top (required)>'
     # ./spec/rails_helper.rb:79:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:108:in `block (2 levels) in <top (required)>'

Top 2 slowest examples (0.29972 seconds, 71.6% of total time):
  Payment Association should belong to payable required: true
    0.28796 seconds ./spec/models/payment_spec.rb:18
  Payment Validation #amount_is_valid not charge more than event balance
    0.01176 seconds ./spec/models/payment_spec.rb:32

Finished in 0.4186 seconds (files took 4.31 seconds to load)
2 examples, 2 failures

Failed examples:

rspec ./spec/models/payment_spec.rb:18 # Payment Association should belong to payable required: true
rspec ./spec/models/payment_spec.rb:32 # Payment Validation #amount_is_valid not charge more than event balance


更新

根据 Schwern 的反馈通过规范。 仍然对金额使用自定义验证,因为 balance 是关联 payable 上的一个字段,而不是 payment 上的一个字段(无法找到从内部访问关联模型的方法-在验证助手中)

# payment.rb

class Payment < ApplicationRecord

  belongs_to :payable, polymorphic: true

  validates :payable, presence: true 
  validate :amount_is_valid 
  
  def amount_is_valid
    if amount > payable.balance
      errors.add(:amount, "can't be greater than balance")
    end
  end

end

# spec/models/payment_spec.rb

require 'rails_helper'

RSpec.describe Payment, type: :model do
  
  let(:event) { FactoryBot.create(:event, event_type: 'test', total: 10000, balance: 10000) }
  let(:user) {FactoryBot.create(:user)}
  let(:payment) { 
    FactoryBot.build(:payment, 
      amount: 300,
      method: 'cash', 
      payer_id: user.id,
      payable: event, 
      status: 1,
    )
  }

  describe '#payable' do
    it 'is an Event' do
      expect(payment.payable).to be_a(Event)
    end
  end
  
  describe '#amount' do 
    context 'amount is higher than balance' do 
      before {
        payment.amount = payment.payable.balance + 1
      }
      it 'is invalid' do 
        payment.validate
        expect(payment.errors[:amount]).to include("can't be greater than balance")
      end
    end
  end

end

您的第一个测试并没有像您认为的那样失败。它在下一行失败,is_expected.to belong_to(:payable).

您正在设置 payment,但您正在测试 implicitly defined subject,这将是 Payment.new

is_expected.to belong_to(:payable)

相当于...

expect(subject).to belong_to(:payable)

并且由于您没有定义 subject 这是...

expect(Payment.new).to belong_to(:payable)

Payment.new 没有定义 payable,因此 amount_is_valid 验证错误。

要解决此问题,请直接测试 payment。我建议您在学习 RSpec 时远离 subject。而且你不应该设置payment.event,它已经在工厂设置了。

describe 'Association' do
  expect(payment).to belong_to(:payable)
end

但我不知道 belong_to 匹配器。您不应该直接检查实现,而应该检查它的行为。您想要的行为是 payment.payable 到 return a Payable.

describe '#payable' do
  it 'is a Payable' do
    expect(payment.payable).to be_a(Payable)
  end
end

第二次失败是因为您没有正确初始化您的支付。您正在传递 payable_id: event.id,但不会设置 payable_type。没有 payable_type 它不知道 class 这个 ID 的用途。

相反,直接传递对象。

let!(:payment) { 
  FactoryBot.build(:payment, 
    amount: 300,
    method: 'cash', 
    payer: user,
    payable: event, 
    status: 1,
  )
}

一些更一般的清理...

  • let! 将始终 运行 块,无论是否使用。除非您特别需要,否则请使用 let 并且块将根据需要 运行。
  • 您希望 payable 存在,因此验证 payable 是否存在。
  • 使用内置的 numericality validator 金额。
class Payment < ApplicationRecord

  belongs_to :payable, polymorphic: true
  validates :payable, presence: true

  validates :amount, numericality: {
    less_than_or_equal_to: balance,
    message: "must be less than or equal to the balance of #{balance}"
  }
end
require 'rails_helper'

RSpec.describe Payment, type: :model do
  let(:event) {
    create(:event, event_type: 'test', total: 10000, balance: 10000)
  }
  let(:user) { create(:user) }
  let(:payment) {
    build(:payment, 
      amount: 300,
      method: 'cash', 
      payer: user,
      payable: event,
      status: 1
    )
  }

  # It's useful to organize tests by method.
  describe '#payable' do
    it 'is a Payable' do
      expect(payment.payable).to be_a(Payable)
    end
  end

  describe '#amount' do
    # Contexts also help organize and name your tests.
    context 'when the amount is higher than the payable balance' do
      # This code will run before each example.
      before {
        # Rather than hard coding numbers, make your tests relative.
        # If event.balance changes the test will still work.
        payment.amount = payment.payable.balance + 1
      }
    
      it 'is invalid' do 
        expect(payment.valid?).to be false
        expect(payment.errors[:amount]).to include("must be less than or equal to")
      end
    end
  end
end