I have this Task model:
class Task < ActiveRecord::Base
acts_as_tree :order => 'sort_order'
end
And I have this test
class TaskTest < Test::Unit::TestCase
def setup
@root = create_root
end
def test_destroying_a_task_should_destroy_all_of_its_descendants
d1 = create_task(:parent_id => @root.id, :sort_order => 2)
d2 = create_task(:parent_id => d1.id, :sort_order => 3)
d3 = create_task(:parent_id => d2.id, :sort_order => 4)
d4 = create_task(:parent_id => d1.id, :sort_order => 5)
assert_equal 5, Task.count
d1.destroy
assert_equal @root, Task.find(:first)
assert_equal 1, Task.count
end
end
The test is successful: when I destroy d1, it destroys all the descendants of d1. Thus, after the destroy only the root is left.
However, this test is now failing after I have added a before_save callback to the Task. This is the code I added to Task:
before_save :update_descendants_if_necessary
def update_descendants_if_necessary
handle_parent_id_change if self.parent_id_changed?
return true
end
def handle_parent_id_change
self.children.each do |sub_task|
#the code within the loop is deliberately commented out
end
end
When I added this code, assert_equal 1, Task.count
fails, with Task.count == 4
. I think self.children
under handled_parent_id_change
is the culprit, because when I comment out the self.children.each do |sub_task|
block, the test passes again.
Any ideas?
I found the bug. The line
d1 = create_task(:parent_id => @root.id, :sort_order => 2)
creates d1. This calls the before_save
callback, which in turn calls self.children
. As Orion pointed out, this caches the children of d1.
However, at this point, d1 doesn't have any children yet. So d1's cache of children is empty.
Thus, when I try to destroy d1, the program tries to destroy d1's children. It encounters the cache, finds that it is empty, and a result doesn't destroy d2, d3, and d4.
I solved this by changing the task creations like this:
@root.children << (d1 = new_task(:sort_order => 2))
@root.save!
This worked so I'm ok with it :) I think it is also possible to fix this by either reloading d1 (d1.reload
) or self.children (self.children(true)
) although I didn't try any of these solutions.
children
is a simple has_many association
This means, when you call .children
, it will load them from the database (if not already present). It will then cache them.
I was going to say that your second 'test' will actually be looking at the cached values not the real database, but that shouldn't happen as you are just using Task.count
rather than d1.children.count
. Hrm
Have you looked at the logs? They will show you the SQL which is being executed. You may see a mysql error in there which will tell you what's going on
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With