Jake Scruggs

writes ruby/wears crazy shirts

Recently I spent some time away from RSpec and I did miss it, but one thing especially liked about the testing framework I lived in for 10 months was that it had two ways to test anything you liked: One that hit the database and one that did not. We used UnitRecord to disconnect unit tests from the database (I should mention that we defined unit tests as tests that didn't hit the db and functional tests as those that did). So the vast majority of our tests didn't really need to hit the db and ran very fast. Sometimes we wanted or needed to hit the db and we did (only occasionally in model tests, more often in controller tests, and we kept logic out of the view and only tested views at the Selenium level). It was pretty cool and I'd like to thank the team who set it up before I got there.

There's been a lot of talk about whether model, view, or controller tests should hit the db but I've found that there's enough exceptions that it's worth just saying that you should try not to hit the db when possible for speed's sake and then when the test becomes 20 lines long because of all the mocking/stubbing required, go ahead and hit it. That's all well and good, but how do you disconnect the db from an RSpec example?

I, personally, have had success with NullDB. NullDB is the null object pattern applied to databases. Which is a fancy way of saying that it swallows up any call that would go to the db and returns a nil. So you mock/stub all you like, but if you forget or just don't care about a particular call then don't worry because nothing goes to the db and nil is returned (which, surprisingly, is more than enough in most situations). On the NullDB page I've linked to, the author provides a way to universally disconnect RSpec from the db, but, as I've already said, I'm a bit pragmatic when it comes to disconnection so In my spec_helper.rb I define something like this:

 1 share_as :Disconnected do
3 before :all do
4 ActiveRecord::Base.establish_connection(:adapter => :nulldb)
5 end
7 after :all do
8 ActiveRecord::Base.establish_connection(:test)
9 end
10 end

and then in any "Describe" block where I'd like to disconnect the db I do this:

1 describe User do
2 include Disconnected
3 # 'it' blocks or
4 # more describes
5 end

If I want to hit the db I just don't include Disconnected.

Now since NullDB fails silently I've heard tell of problems with it being hard to debug (although I have yet to experience these problems). Any spec/test/example that just isn't behaving properly can be exiled to the 'hit the database' area. Also I should mention that when I tried to use NullDB with Rails 1.2.6 I got a bunch of errors but it worked fine with 2.0.2. If you are looking for a more vigorous enforcement of 'don't touch the db in unit tests' then UnitRecord may be for you. I thought RSpec and UnitRecord didn't play nice, but my fellow Obtivian, Ryan Platte informs me that he's got it working. So I'm calling him out to post about disconnecting the RSpec with UnitRecord.


Anonymous said...

You might be interested to know that rspec 1.1.4 shipped with the stub_model() method, which instantiates a normal Active Record, but raises an exception for any attempt to access the database. It's very close to the NullDB pattern and I have been using it with great success.

Ryan Platte said...

One big reason to disconnect globally is that by default Rails wants to run the db:test:prepare Rake task before running any tests/specs at all, which is a Whole Lotta SQL to sit through when you just want to hit your unit tests quickly.

So (I will try to post more soon, but can't promise when) separating out a test/spec task that doesn't have db:test:prepare as a dependency is a great way to go to achieve a fast test run (as well as the other benefits to true unit testing).

Jake Scruggs said...

as to stub_model, I think it hits the database the first time and then stops future connections. There was an interesting discussion of this on the Chirb mailing list (Chicago Ruby Users Group) here:

Karmen Blake said...

any word on rails 2.1 compatibility? I was using this until I upgraded to 2.1. I really like this plugin.