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 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
class CreateInitialSchema < ActiveRecord::Migration def self.up create_table :topics, :force => true do |t| t.column :title, :string t.column :created_on, :datetime t.column :state, :string, :default => 'visible' end create_table :posts, :force => true do |t| t.column :content, :text t.column :author, :string t.column :topic_id, :integer t.column :created_on, :datetime t.column :state, :string, :default => 'visible' end end def self.down drop_table :posts drop_table :topics end end
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
def Topic < ActiveRecord::Base has_many :posts validates_uniqueness_of :title end
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
def Post < ActiveRecord::Base belongs_to :topic validates_uniqueness_of :content, :scope => :topic_id 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).
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.