【问题标题】:Active Record Association CollectionProxy not working: why does @owner.@target.count yield a result of 1 when @owner.@target returns an empty array?Active Record Association CollectionProxy 不工作:当@owner.@target 返回一个空数组时,为什么@owner.@target.count 的结果为1?
【发布时间】:2015-07-27 11:22:39
【问题描述】:

我的 Rails 应用中有模型:

拥有_many Swots 的Sales_Opportunity。

我正在使用 FactoryGirl 设置它们并运行测试以显示当我删除我的 Sales_Opportunity 时,我也会删除关联的 Swots。出于某种原因,在使用 Byebug 进行调试时,我得到了奇怪的结果——Sales_Opportunity 和 Swot 记录被正确创建,但是当我运行 sales_opportunity.swots 它返回 [ ] 而 sales_opportunity.swots.count 返回 1。更奇怪的是确切的相同的代码适用于花药对象关联(timeline_events 与 swot 完全一样,但适用于相同的代码)。

谁能告诉我我做错了什么?

Sales_Opportunity.rb:

class SalesOpportunity < ActiveRecord::Base
 default_scope { order('close_date ASC') }
 belongs_to :user
 belongs_to :company
 has_many :key_contacts
 has_many :timeline_events, dependent: :destroy
 has_many :swots, dependent: :destroy
end

Swot.rb:

class Swot < ActiveRecord::Base
 belongs_to :sales_opportunity
 validates :swot_details, presence: true
 validates :sales_opportunity_id, presence: true
 enum swot_type: [:strength, :weakness, :opportunity, :threat]
 enum swot_importance: { minimal: 1, marginal: 2, noteworthy: 3, significant: 4, critical: 5 }
 validates :swot_importance, presence: true
end

Swot FactoryGirl 规格:

FactoryGirl.define do
factory :swot do
    swot_importance             "minimal"
    swot_details                "Some boring details"
    swot_type                   "threat"

    trait :attached do
        association             :sales_opportunity, :with_user_id
    end
 end
end

Sales_Opportunity FactoryGirl 规范:

FactoryGirl.define do
sequence(:opportunity_name) { |n| "Sales Oppotunity - #{n}" }

factory :sales_opportunity do
    user
    opportunity_name {generate(:opportunity_name)}
    close_date                  "2014/12/12"
    sale_value                  10000
    company_id                  7

    trait :with_user_id do
        user_id                     6
    end
end
end

Rspec 测试失败:

describe "when swot's parent sales opportunity is destroyed" do
    let(:swot) { FactoryGirl.create(:swot, :attached) }
    let(:sales_opportunity) { swot.sales_opportunity }

it "should destroy associated swots" do
    dswots = sales_opportunity.swots.to_a
    byebug
    sales_opportunity.destroy
    expect(dswots).not_to be_empty
    dswots.each do |dswot|
    expect(Swot.where(id: dswot.id)).to be_empty
  end
end
  end

记录 swot 时从控制台输出(byebug):

#<Swot id: 13, swot_type: 3, swot_importance: 1, sales_opportunity_id: 564, swot_details: "Some boring details", created_at: "2015-07-27 10:57:23", updated_at: "2015-07-27 10:57:23">

记录 sales_opportunity 时控制台的输出:

#<SalesOpportunity id: 564, close_date: "2014-12-12 00:00:00", user_id: 6, created_at: "2015-07-27 10:57:23", updated_at: "2015-07-27 10:57:23", pipeline_status: 0, opportunity_name: "Sales Oppotunity - 4", company_id: 7, sale_value: #<BigDecimal:7fe9ffd25078,'0.1E5',9(27)>, swot_score: 0>

sales_opportunity.swots.count 的输出:

(byebug) sales_opportunity.swots.count
 1

sales_opportunity.swots 的输出:

(byebug) sales_opportunity.swots
#<ActiveRecord::Associations::CollectionProxy []>

我想我已经包含了所有已知信息。 Rspec 测试、FactoryGirl 工厂和 sales_opportunities 和 Swots/Timeline_Events 之间的设置完全相同 - 但是 Timeline_Events 的 Rspec 测试通过并且 collection_proxy 适用于那些(据我所知,代码是相同的):

Timeline_Event 工厂:

FactoryGirl.define do
factory :timeline_event do
    activity                    "Some activity"
    due_date                    "2014/11/11"

    trait :attached do
        association             :sales_opportunity, :with_user_id
    end
end
end

工作 Rspec 测试:

describe "when sales opportunity is destroyed for timeline event" do
    let(:timeline_event) { FactoryGirl.create(:timeline_event, :attached) }
    let(:sales_opportunity) { timeline_event.sales_opportunity }

it "should destroy associated timeline events" do
    timeline_events = sales_opportunity.timeline_events.to_a
    sales_opportunity.destroy
    expect(timeline_events).not_to be_empty
    timeline_events.each do |event|
    expect(TimelineEvent.where(id: event.id)).to be_empty
  end
end
end

Timeline_Event.rb:

class TimelineEvent < ActiveRecord::Base
 belongs_to :sales_opportunity
 validates :activity, presence: true 
 validates :due_date, presence: true
 validates :sales_opportunity_id, presence: true
end

在这里的同一个地方运行 byebug 时,我得到一个包含 Timeline_Event 的数组。

谁能帮我理解我的代码出了什么问题?

谢谢。

【问题讨论】:

  • 永远不要在循环中添加测试期望 - 这被认为是一种非常糟糕的做法。
  • 好的,您对我如何改进此代码有什么建议吗?实现相同结果的更好方法是什么? (我从另一个网站复制了这个 - 它实现了我的目标,所以我没有质疑它的真实性)

标签: ruby-on-rails ruby-on-rails-3 rspec byebug


【解决方案1】:

我解决了这个问题 - 似乎需要重新加载 sales_opportunity 才能保持 Active Record 关联。 This answer 是解决问题的关键。

这是工作代码:

    describe "when swot's parent sales opportunity is destroyed" do
     let!(:swot) { FactoryGirl.create(:swot, sales_opportunity: sales_opportunity) }

it "should destroy associated swots" do
    sales_opportunity.reload
    expect { sales_opportunity.destroy }.to change(sales_opportunity.swots, :count).by(-1)
 end
end

使用上面 Max 回答的一些元素也帮助我改进了代码的外观和感觉。

【讨论】:

    【解决方案2】:
    RSpec.describe SalesOpportunity, type: :model do
    
      let(:sales_opportunity) { create(:sales_opportunity) }
    
      describe "swots" do
        let!(:swot) { create(:swot, sales_opportunity: sales_opportunity) }
    
        it "destroys nested swots" do
          sales_opportunity.destroy
          swot.reload
          expect(swot.destroyed?).to be_truthy
        end
      end
    end
    

    请注意,我将其添加到 SalesOpportunity 规范中,因为依赖的销毁行为属于 SalesOpportunity 而不是子关联。

    编辑。

    编写此规范的另一种方法是:

    it "destroys nested swots" do
      expect {
        sales_opportunity.destroy
      }.to change(Swot, :count).by(-1)
    end
    

    【讨论】:

    • 谢谢,我正在使用这段代码,但我仍然遇到同样的问题 - 可能是由于我的 before_save 和 after_save 回调(我没有包含在原始代码中,但实际上可能导致了这个行为)。我将再次更新主要问题,因为这可能是我的问题。我将使用您的代码进行时间线事件测试。
    • 不幸的是,这也不适用于timeline_event - 当我点击 sales_opportunity.destroy 然后使用 swot/timeline_event.reload 我得到一个ActiveRecord::RecordNotFound Exception: Couldn't find TimelineEvent with 'id'=121 - 似乎 ActiveRecord 无法重新加载一个项目已被摧毁。我可以改用expect(swot.reload).to be_falsy 作为测试吗?
    • 嗯,这是一种积极的结果:)。我认为您可以删除 timeline_event.reload 。我有点确定是否需要它,但是当我想到它时,它被摧毁了吗?应该对数据库进行计数查询,这样您就不必担心内存中的实例没有同步
    • 确实 - 我很感激帮助我朝着正确的方向前进。 .破坏?不起作用(似乎内存实例未同步)所以我想知道 .reload 将失败的期望测试是否适当全面。
    • 哇,我真的搞砸了,应该是expect(swot.destroyed?).to be_truthy。你可以写expect(TimelineEvent.where(id: timeline_event.id).count).to eq 0
    猜你喜欢
    • 1970-01-01
    • 2021-10-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-09
    • 1970-01-01
    • 2015-08-26
    相关资源
    最近更新 更多