0

I have models in my Rails app:

Sales_Opportunity which has_many Swots.

I'm setting them up using FactoryGirl and running a test to show that when I delete my Sales_Opportunity I also cause the associated Swots to be deleted. For some reason when debugging with Byebug I'm getting strange results - the Sales_Opportunity and Swot records are being correctly created, but when I run sales_opportunity.swots it returns [ ] whereas sales_opportunity.swots.count returns 1. What's more odd is the exact same code works fine with anther object association (timeline_events are exactly like swots yet work perfectly with the same code).

Can anyone tell me what am I doing wrong please?

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 spec:

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 spec:

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

Failing Rspec tests:

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

Output from the console (byebug) when logging swot:

#<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">

Output from the console when logging 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>

Output for sales_opportunity.swots.count:

(byebug) sales_opportunity.swots.count
 1

Output for sales_opportunity.swots:

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

I think I've included all the known info. The Rspec tests, FactoryGirl factories and setup between sales_opportunities and Swots/Timeline_Events is exactly the same - yet the Rspec tests pass for Timeline_Events and the collection_proxy works for those (so as far as I can tell, the code is identical):

Timeline_Event Factory:

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

Working Rspec tests:

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

When running byebug in the same place here I get an array including the Timeline_Event.

Can anyone help me understand what's going wrong in my code?

Thanks.

Zoinks10
  • 619
  • 6
  • 21
  • Never add test expectations in a loop - it is regarded as a really poor practice. – max Jul 27 '15 at 11:53
  • OK, do you have suggestions of how I could improve this code? What would be a better way of achieving the same result? (I copied this from another website - and it achieved my objectives so I didn't question its veracity) – Zoinks10 Jul 27 '15 at 12:52

2 Answers2

1
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

Note that i'm adding this to the SalesOpportunity spec, because the dependant destroy behavior belongs to SalesOpportunity not the child association.

Edit.

Another way to write this spec would be:

it "destroys nested swots" do
  expect {
    sales_opportunity.destroy
  }.to change(Swot, :count).by(-1)
end
max
  • 96,212
  • 14
  • 104
  • 165
  • Thanks, I'm using this code but I still have the same issues - possibly due to my before_save and after_save callbacks (which I didn't include from the original code, but may actually be causing this behaviour). I'll update the main question again as this is probably my issue. I'll use your code for the timeline_events test. – Zoinks10 Jul 27 '15 at 14:35
  • Unfortunately this doesn't work with the timeline_event either - when I hit sales_opportunity.destroy and then use swot/timeline_event.reload I get an `ActiveRecord::RecordNotFound Exception: Couldn't find TimelineEvent with 'id'=121` - it seems ActiveRecord can't reload an item that has been destroyed. Could I use `expect(swot.reload).to be_falsy` as the test instead? – Zoinks10 Jul 27 '15 at 15:00
  • Well thats a kind of positive result :). I think you can just remove timeline_event.reload . I was a bit ensure if it was need but when I think about it .destroyed? Should do a count query on the db so you dont have to worry about the in memory instance not being synced – max Jul 27 '15 at 16:58
  • Indeed - I appreciate the help nudging me in the right direction. .destroyed? doesn't work (it seems the memory instance isn't synced) so I wondered whether an expectation test that .reload would fail would be suitably comprehensive or not. – Zoinks10 Jul 28 '15 at 01:06
  • Wow, I really messed that one up, should be `expect(swot.destroyed?).to be_truthy`. You could write `expect(TimelineEvent.where(id: timeline_event.id).count).to eq 0` – max Jul 28 '15 at 10:38
  • Thanks - that might be a better option than mine (where I'm expecting the specific case of reducing the number of Timeline Events by one) as yours actually tests that the entire array has gone. If I do that, presumably I should also test that the array doesn't eq(0) before I destroy the sales opportunity though. – Zoinks10 Jul 29 '15 at 00:34
1

I solved the issue with this - it seems the sales_opportunity needs to be reloaded for the Active Record Associations to be persisted. This answer is the key to the solution.

Here's the working code:

    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

Using some elements of Max's answer above also helped me improve the look and feel of the code.

Community
  • 1
  • 1
Zoinks10
  • 619
  • 6
  • 21