Friends Don't Let Friends Hit The Database In Tests

Nov 15, 2012 • Aaron Jensen

Unit tests that hit the database are generally slower than those that don’t by an order of magnitude. Not hitting the database in specs often makes your code better as well. You are forced to stub out collaborators that would hit the database and stubbing things out forces you to think more about the actual interface. Suddenly things like

Video.published.for_user(user).most_interesting_first.limit(4)

become red flags when called from a controller. They make you do things like stub_chain which is usually just a tool to enable Law of Demeter violations. You’d likely be better served by a more specific scope or, even better, a query object.

Yes, you could use a sqlite in memory database to potentially speed things up but you’re still going to be much slower than a test that simply stubs the database access.

To this end, we’ve decided to prevent access to the database from specs that don’t explictly request or “naturally” need it. A test will fail if it hits the database.

Why not just be diligent about it? A couple reasons. First and foremost, we like to encourage the Pit of Success. The more we can do to make it difficult to do bad things the better. Also, Rails makes it pretty hard to know when you’re hitting the database.

To make sure a test fails when it hits the database, we freedom patch the Mysql2Adapter and add some RSpec configuration. This could likely be ported to minitest or TestUnit or whatever other framework you fancy.

ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_evaldodefexecute_with_prevent_database_access(sql,name=nil)ifprevent_database_access?(sql)raise"You should only access the database in model and acceptance specs. If you really need to you can use :db to grant access for that spec. Offending query: \"#{sql}\""endexecute_without_prevent_database_access(sql,name)endalias_method_chain:execute,:prevent_database_accessdefprevent_database_access?(sql)returnfalseunlessActiveRecord::ConnectionAdapters::Mysql2Adapter.prevent_database_accesssql=~/^(SELECT|UPDATE|INSERT|DELETE)/endclass<<selfattr_accessor:prevent_database_accessendendRSpec.configuredo|config|config.backtrace_clean_patterns<<%r{#{__FILE__}}config.before:eachdo# Prevent database access unless :db is true or we're in a :request # or :model spec. Customize to suit your needs.ActiveRecord::ConnectionAdapters::Mysql2Adapter.prevent_database_access=!(example.metadata[:db]||[:request,:model].include?(example.metadata[:type]))endend

A few things to note:

Adding this to an existing project will take some work, but it will likely expose plenty of things to talk about as it did for us.

We only prevent SELECT, UPDATE, INSERT and DELETE. Things like stub_model need to hit the database to get column information. We don’t want to stop this.

Request specs, model specs and any spec with :db => true will be allowed to hit the database. You can customize this in the config.before :each block above.

You also may want to check out nulldb which will let you run tests against a fake database. This can speed things up when you want to test things like observers or after_save hooks.