Opensource Development and Life
RSS icon Email icon Home icon
  • Libraries, exceptions, and debugging

    Posted on June 26th, 2009 Quad341 No comments

    Seeing and Solving the Problem

    We now have a couple neat models to play with! To make sure everything works right, we should write unit tests! Since I’m lazy and only am looking to show off an error, I’ll essentially hand write one single unit test instead of doing it correctly. Actually creating the unit test would just involve asserting what I’ll ask you to confirm visually (and you could verify all sorts of other states and such; check out Factory Girl for a great factory framework for tests and shoulda for a nice unit test extension, both by thoughtbot).

    We are going to use fixtures here so it’s easier to set up our environment again. Since we have an extremely simple environment, using fixtures isn’t really needed, but they are useful in larger projects and can potentially lead to headaches like one I’m about to show you.

    For this, we’re just going to have a single topic, so toss this into your test/fixtures/topics.yml file:

    1
    2
    3
    
    default_topic:
      id: 1
      title: Default Topic

    We don't look for much here. Since we have some initial values and ActiveRecord will take care of created_on, we can leave that much data without any worries. Now let's make a few posts (test/fixtures/posts.yml):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    post_001:
      id: 1
      topic_id: 1
      author: Joe
      content: Good content
     
    post_002:
      id: 2
      topic_id: 1
      author: Gwen
      content: Ok content
     
    post_003:
      id: 3
      topic_id: 1
      author: Chris
      content: Bad content
      state: flagged
     
    post_004:
      id: 4
      topic_id: 1
      author: Dan
      content: Good content

    In case you didn't catch the error in there, post_001 and post_004 have the same content in the same topic. This is a violation of the validator we set up earlier. While it's easier to notice with constraints on text, small errors in numeric columns with larger quantities of data are common.

    (To actually load the fixtures, you need to edit config/databases.yml to accurately reflect a database setup for you and import the schema via rake db:migrate. SQLite is more than sufficient for our needs here)

    Now we load the fixtures. If we execute rake db:fixtures:load, rake does some work and comes back without errors. Well... that's odd. We know we had an error but rake doesn't think so. This is important to know that when loading fixtures, rake is just dumping the data into the database directly and ignoring your models. This means that you can get invalid or garbage data into your database that your models otherwise would have protected you against.

    If we didn't know this was wrong, we might want to go and play with our models (or write unit tests for them). So let's fire up script/console and try this out. If we were to load the first post (p = Post.find(1)), would could try to, say, hide it: (p.hide!). If you just use p.hide, you'll get the response of false. Using hide! will actually raise the exception so we can look at it. The exception comes from inside of our plugin and is an invalid state transition. The message will tell us that you cannot transition via :hide from "visible". That's just not true! We very clearly told it that we wanted it.

    Since we aren't sure what actually happened, we need to narrow down the problem. We verify our transition is correct, so we might suspect there might be a bug in the library (it happens; the authors are only human, generous as they might be). A good way to verify that is to avoid the library and do its job yourself. Let's comment out the transition and write the equivalent code ourselves:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    
    def Post < ActiveRecord::Base
      belongs_to :topic
      validates_uniqueness_of :content, :scope => :topic_id
     
      after_destroy { |post| post.topic.destroy! if post.topic.posts.count < 1 }
     
      state_machine :state, :initial => :visible do
        # add some notification to admin after a post is flagged?
        after_transition :on => :hide, :do => :consider_hiding_topic
     
        event :flag do
          transition :visible => :flagged
        end
     
        event :approve do
          transition :flagged => :visible
        end
     
        #event :hide do
        #  transition :visible => :hidden
        #end
     
        event :show do
          transition :hidden => :visible
        end
     
        event :hide_for_topic
          transition :visible => :hidden_for_topic
        end
     
        event :show_for_topic
          transition :hidden_for_topic => :visible
        end
      end
     
      def hide
        state = 'hidden'
        save
      end
     
      def hide!
        state = 'hidden'
        save!
      end
     
      protected
     
      def consider_hiding_topic
        self.topic.hide! if self.topic.posts.with_state('visible').count < 1
      end
    end

    We simply removed the transition and added the equivalent other methods. Let's fire up script/console again and try this once more. This time if you run Post.find(1).hide!, we'll see and ActiveRecord validation error. Of course! Our fixture has an error in it making it so the item can't save and an exception is thrown! You can change the content of 001 or 004 and reload the fixtures to see everything become happy. Once it's working again, you should return to using state_machine (and make sure you reload irb between edits to your model so you don't run into any caching problems).

    Conclusion

    So what did this show? Exceptions are a great tool for debugging. The problem occurs when you can't see the actual exception and instead get a library's generic (and in this case, misleading) exception instead. So what's the solution? If you are ever setting up a library, ensure you only catch exceptions you intend to and then either handle them appropriately or throw a new exception that explains what actually happened. Don't use catch-all statements (like catch Exception or a generic rescue with no type) anywhere you aren't very sure you really want to get _everything_ that might have just gone wrong. That usually is only at the last level before your application is shown to your user. At that point, you do want to catch all exceptions and simply log what happened. A good program will never show an exception to the user. An error that happens to have the same message is different so long as the program does not crash (as uncaught exceptions will cause).

    If you use your exceptions well, they are a great tool. If you silence your exceptions, they can't help you and won't let you know when things are going wrong. To be really robust, make sure your unit tests do check error cases and ensure the correct exceptions are being thrown at the correct times.

    Leave a reply