Friday, January 18, 2008

Spiking with reckless abandon



A couple months ago, we were trying to improve our build process to better reflect our production environment. Our application runs on Oracle and the database schema is owned by one Oracle user, and our application logs in as another. The application user is granted rights to the tables it needs to use. There are also synonyms defined for all the tables our app uses.

Our builds only used a single database user for all our tests. If we forgot to update our database scripts to add new grants or synonyms, we wouldn't know it until we deployed to our QA environment. So, our goal was to have our build run with the same user split that the production environment did.

One problem with that scenario is our functional tests inserted data into tables that the app user didn't have insert rights to. We decided to spike switching to the schema owner for insertion of test data and go back to the regular app user for the rest of the test. We came up with this crazy bit of code to hijack our ActiveRecord models and reuse them to insert test data:

module TestData
def self.const_missing(name)
klass = Kernel.const_get(name)
new_klass = Class.new(klass)
const_set(name, new_klass)

new_klass.class_eval do
establish_connection "test_db_owner"

def ==(comparison_object)
comparison_object.equal?(self) ||
(comparison_object.instance_of?(self.class.superclass) &&
comparison_object.id == id &&
!comparison_object.new_record?)
end

def instance_of?(klass)
self.class == klass || self.class.superclass == klass
end
end

new_klass
end
end



This bit of code defines a new module called TestData. It overrides const_missing? and looks for an existing constant in the root namespace. It creates a new class in the TestData module that is a subclass of the original class. It then tells ActiveRecord to connect as the schema owner. There is some stuff in there to patch == and instance_of? to allow the new object to be compared to the original one.

Now, code like this is quite dangerous and is nothing I'd ever use in real production code. The real point of the spike however, was not figuring out how to connect as a different user, but to see how feasible it would be to modify our tests to use two database users. This little hack only took about 20 minutes to write and allowed us to attack our main problem. Since this was a spike, we knew we were going to throw it all away after we learned what we needed to know.

It turned out that we ran we couldn't use two seperate users easily, because all our test methods ran in transactions. We relied on the transaction rollback to clean up all our inserted test data. We could work around this, but it would take a lot more rework than we had originally planned. Our little TestData module was promptly deleted from our code base.

I am a big fan of any language that gives power to the developer. I don't like to be shackled because some language designer thinks that developers should be protected from themselves. Features like Ruby's const_missing? can be horribly misused and can cause code that is extremely difficult to debug, but it can also be really useful. Our little experiment only took a couple of hours, because we were able to use a powerful, but possibly dangerous feature of Ruby. That module was the quickest way for us to get to the meat of the problem without wasting a lot of time on technical details that we could figure out once we committed to doing the work.

I believe in smart tools for smart developers. Making a language overly safe doesn't really enforce code quality. Bad programmers will write bad code no matter the language. All it does is keep the good ones from accomplishing what they need to do.