【问题标题】:Rails Rspec - How to set polymorphic has_many associationRails Rspec - 如何设置多态 has_many 关联
【发布时间】:2020-07-31 19:31:49
【问题描述】:

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

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

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

在规范中建立这种关联的最佳方式是什么?

# 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

【问题讨论】:

    标签: ruby-on-rails rspec


    【解决方案1】:

    您的第一个测试并没有像您认为的那样失败。它在下一行失败,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 返回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,它不知道这个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
    

    【讨论】:

    • 谢谢!非常感谢深入的回应和额外的建议。我的规格现在正在通过。一件事 - 忘了在原始帖子中提到我使用的是shoulda-matchers gem,它提供了belongs_to 助手。
    • @orangesoda 不客气。试试 rubocop-rspec,它会发现很多像你一样的问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-21
    • 2014-12-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-20
    相关资源
    最近更新 更多