Rails: UnitRecord - Test without the Database

Tests Need to Run Quickly

Finished in 11.541093 seconds.

3001 tests, 5325 assertions, 0 failures, 0 errors

I gave a lightning talk on testing at the Ruby Hoedown conference last weekend. Emphasizing the need for a test suite to run quickly, I ran through how my ThoughtWorks team tests differently than the basic Rails setup. The metric above is from our unit test suite. An important note is that I'm not using the term "unit test" the same way Rails does. We unit test our models without hitting the database. We also unit test our controllers, testing them without rendering the view and with mocking the models.

To run tests without hitting the database, my team was using this code from Muness Alrubaie, based on the original implementation from Jay Fields. Big props and thanks to Jay for his work on this - disconnecting unit tests from the database is a huge time saver and productivity gain for a development team. Other people have made their own variations.

Weaknesses of the Previous Implementation

However, this implementation has few weaknesses:

  1. The SQL database type has to be inferred from the classes of the values passed in the constructor. For example, if I call Person.new :first_name => "Dan" it stubs the name column as a :string type. This is generally okay, but if I call Person.new :name => nil, what should the type be? Making this inference can cause inconsistencies between the tests and actual behavior, which is a problem.
  2. Constructor calls may need attributes not actually needed by the test. Because the columns are determined in the initialize method, they are always determined based on the attributes passed. This means that even if I only care about the first_name for a particular test, I may also have to pass in a last_name to get the column stubbed out. Doing this adds unnecessary noise to tests.
  3. Columns can be stubbed which do not actually exist. You can argue that this can happen in Ruby anytime you're mocking / stubbing, but it's not good. Also, in the normal ActiveRecord initialize, you can pass in values that will be sent to attribute writers. However, with this way of disconnecting from the database, those attributes will become columns. This has the same problem as the first point: the behavior in the test suite differs from the actual behavior.

While moving through iterations of improvements to prevent these issues, we had to fix quite a few tests. All of them were doing something that would not work if it wasn't for the "specialized" constructor. We ended up developing a much better solution: we determine columns from the schema.rb file. This enables us to have really fast unit tests that do not hit the database, while solving the problems outlined above of having slightly different behavior. Each model has the columns defined exactly as in the actual database.

UnitRecord Gem

I wrapped the new and improved approach into a gem called "UnitRecord." This naming may be confusing since Rails refers to unit tests as any model test. However, when testing your application's business logic, you should do just that. Testing the ActiveRecord framework in addition to your business logic is unnecessary - ActiveRecord has its own suite of tests. Therefore, disconnecting from the database allows you to test the business logic in isolation from the persistence logic and have really fast tests.

Source Code and Installation

gem install unit_record

Using the gem involves a few steps covered in the documentation.

Additional Info

Jay Fields wrote a blog entry on how we test. UnitRecord is one piece of the picture. We also use a factory instead of fixtures, and define tests using dust.

If you're using autotest, restrcturing your test directory to support UnitRecord will break autotest. Jamie Hill wrote a plugin to autoest with unitrecord

Geoffrey Grosenbach wrote a rake task to convert the Rails test directory structure. Subversion commits are built-in, so you may want to try it out on a branch or look closely before running it.

Dan Manges Hi, I'm Dan Manges. I'm currently building Root. Previously I was the founding CTO of Braintree. Send me an email.
blog comments powered by Disqus