简体   繁体   English

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

[英]Rails Rspec - How to set polymorphic has_many association

I have a model (payment) that belongs to another model (event), via a polymorphic association.我有一个 model(付款),它属于另一个 model(事件),通过多态关联。

Some tests are failing because the owner model (event) is accessed by the payment model in validations, but the event is returning nil.某些测试失败,因为所有者 model 在验证中访问了所有者 model(事件),但事件返回 nil。 All the features work fine when testing app directly in the browser.直接在浏览器中测试应用程序时,所有功能都可以正常工作。 I added some more comments to payment.rb below.我在下面的payment.rb中添加了更多评论。

I've tried defining the association in the factories, but no luck.我已经尝试在工厂中定义关联,但没有运气。

What is the best way to set up this association in the spec?在规范中设置此关联的最佳方式是什么?

# 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

Both examples in this spec are failing.本规范中的两个示例都失败了。

# 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

Output Output


# 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


Update更新

Passing specs based on Schwern's feedback.根据 Schwern 的反馈通过规范。 Still using a custom validation for amount, because balance is a field on the associated payable , not the payment (couldn't find a way to access an associated model from inside a built-in validation helper)仍在对金额使用自定义验证,因为balance是关联的payable字段,而不是payment (无法找到从内置验证助手内部访问关联 model 的方法)

# 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

Your first test is not failing where you think it is.您的第一个测试并没有在您认为的地方失败。 It's failing on the next line, is_expected.to belong_to(:payable) .它在下一行is_expected.to belong_to(:payable)上失败了。

You're setting payment , but you're testing the implicitly defined subject which will be Payment.new .您正在设置payment ,但您正在测试隐式定义的主题Payment.new

is_expected.to belong_to(:payable)

Is equivalent to...相当于...

expect(subject).to belong_to(:payable)

And since you have no defined subject this is...而且由于您没有定义的subject ,因此...

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

Payment.new does not have payable defined and so the amount_is_valid validation errors. Payment.new没有定义payable ,因此amount_is_valid验证错误。

To fix this, test payment directly.要解决此问题,请直接测试payment And I would suggest staying away from subject while you're learning RSpec.我建议您在学习 RSpec 时远离subject And you should not have to set payment.event , it's already set in the factory.而且您不必设置payment.event ,它已经在工厂中设置。

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

But I'm not aware of a belong_to matcher.但我不知道belong_to匹配器。 You should not be directly checking implementation, but rather its behavior.您不应该直接检查实现,而应该检查它的行为。 The behavior you want is for payment.payable to return a Payable .您想要的行为是payment.payable返回一个Payable

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

The second failure is because you have incorrectly initialized your Payment.第二个失败是因为您错误地初始化了您的付款。 You're passing in payable_id: event.id but that does not set payable_type .您正在传递payable_id: event.id ,但未设置payable_type Without payable_type it doesn't know what class the ID is for.如果没有payable_type ,它不知道ID 的用途是什么class。

Instead, pass the objects in directly.相反,直接传递对象。

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

Some more general cleanups...一些更一般的清理...

  • let! will always run the block whether it's used or not.无论是否使用,都会始终运行该块。 Unless you specifically need that, use let and the blocks will run as needed.除非您特别需要,否则使用let并且块将根据需要运行。
  • You expect payable to exist, so validate the presence of payable .您希望payable存在,因此请验证payable的存在。
  • Use the built in numericality validator on amount.在数量上使用内置的数字验证器
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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM