Hard to find bugs Episode #23221

OK since I have not talked much about snowdevil and how its faring in production I’ll tell you about one of those really hard to find bugs and how to go about fixing it. This is also meant to give a glimpse at how maintenance works in a test driven environment using ruby on rails. I’ll show you how to create the test cases which expose the bugged behavior, how to fix it and how to restore the integrity of your database using rails facilities.

Whenever an exception is thrown on snowdevil I get an email. This has been an invaluable tool so far. This email contains
everything from requested URL to user data (if available) to stack traces, environment variables, params, session and all the rest of the anatomy of the request known to the system.

Yesterday I started getting very odd emails. A user was adding items to his shopping cart but he would get an exception in doing so.

Now this is a fairly well tested concept in this store ( We sold 10 snowboards just last week). So what went wrong?

Turns out that this particular user is a returning customer and on his last visit he left the store with items in his shopping cart which are since deleted. This leads to a “undefined method ‘product’ for nil” type error when the shopping cart is supposed to be displayed. Ouch!

Bugs stink so lets fix this critter.

First lets recreate the issue in a test case. The bug happens when rendering the shopping page so we will go with a functional test case first.
Note that we won’t fix the error in the template, even though this sounds appealing. The error coming from ActionView is only a symptom. We will
fix the cause.


  def test_user_with_deleted_item_in_cart

    # add item one to cart    
    post :add, "id" => 1

    # delete this item
    Product.find(1).destroy
    
    assert_raise(ActiveRecord::RecordNotFound) do
      Product.find(1)
    end

    assert_raise(ActiveRecord::RecordNotFound) do
      ProductUnit.find(1)
    end

    # add another item to cart
    post :add, "id" => 3

    # tell the cart to refresh its items collection. This is done 
    # automatically between real requests
    @request.session["cart"].items(:reload)
        
    # render the cart overview
    get :index
    assert_success
  end

BAM! ActionView::TemplateError: undefined method `product’ for nil:NilClass. Ever been happy about getting an error? Here is your chance!
This is exactly what I have been emailed about by my trusty store.
So now lets fix it, ok? No we do TDD. Lets declare our intention with another unit test:


  def test_deleted_unit
    
    @board1_151cm.destroy
      
    assert_raise(ActiveRecord::RecordNotFound) do
      @cart1_item1.reload
    end
    
  end

Wonderful. This fails as well.

Now equipped with our failing unit tests we can go and write some production code to fix the issue:


  class ProductUnit < ActiveRecord::Base
    has_many :cart_items, :foreign_key => "unit_id", :dependent => true
    [...]
  end

rake tells us that all unit tests work and we fixed a bug. forever and it will never appear again or rake would tell us.

Now off to deploying the changes on the production site and fixing the database retroactively.


tobi@commerce snowdevil $ ruby script/console production
Loading environment...
irb(main):001:0> CartItem.find_all.each { |o| o.destroy if o.unit == nil }

So long and thanks for all the fish.

If you are interested to learn about TDD i recommend the book Test driven development