Testing Rails migrations

I recently found myself searching Stackoverflow and Google for various techniques for automatically testing Rails migrations. I was surprised not to have found too much information though. While testing a migration is fairly straightforward (migrations are classes and you can easily invoke corresponding methods); the challenge can be setting models up properly. That is, migrations are a tool to update an underlying database to reflect changes in models – by the time you are writing a migration, the models already reflect what should be.

Accordingly, to test a migration, you need to set up your test with how things were and then run your migration and verify things have migrated. As it turns out, this is super easy to do with a document oriented database like MongoDB. For this particular project, we’re using Mongoid, which is an Object-Document-Mapper (or ODM) for MongoDB and we’re also using a nifty gem dubbed mongoid_rails_migrations, which facilitates writing Mongoid migrations.

In order to reflect the state of the underlying datastore before running a migration, I needed to remove a particular collection, which due to changes in our models, is automatically populated with meta data when various events occur (think relational trigger here, for example). As you can probably see, as this new collection doesn’t exist in production, all the existing data in production needs some corresponding default meta data to reflect the new requirements which brought this collection to life.

Accordingly, once I initialize my models and the corresponding collection is populated, I need to completely drop the collection. Then, I can run my migration and then verify that the previously nuked collection is present with all the required data.

In my case, I’m using shoulda in concert with Test::Unit, but the details of a test framework really don’t matter – in any case, you’ll need to load your migration, which can be done via a require statement like so:

Requiring a migration
<span class='line-number'>1</span>
<code class='ruby'><span class='line'><span class="nb">require</span> <span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="no">Rails</span><span class="o">.</span><span class="n">root</span><span class="p">,</span> <span class="s1">'db'</span><span class="p">,</span> <span class="s1">'migrate'</span><span class="p">,</span> <span class="s1">'20130217194234_member_app_roles_permissions'</span><span class="p">)</span>

In my fixture logic, I’ve initialized a few objects and related them, which automatically creates the aforementioned meta data collection; consequently, I need to do two things. First, get a connection to the underlying test datastore and then drop that collection.

Obtaining a connection and dropping a collection
<span class='line-number'>1</span>
<span class='line-number'>2</span>
<code class='ruby'><span class='line'><span class="n">db</span> <span class="o">=</span> <span class="vi">@account</span><span class="o">.</span><span class="n">db</span>
</span><span class='line'><span class="n">db</span><span class="o">[</span><span class="s1">'member_app_roles'</span><span class="o">].</span><span class="n">drop</span>

Now I’m ready to run my migration – it’s as simple as invoking the class method up!

Invoking a migration
<span class='line-number'>1</span>
<code class='ruby'><span class='line'><span class="no">MemberAppRolesPermissions</span><span class="o">.</span><span class="n">up</span>

Once the migration has finished running, I can then verify that everything is cool and copasetic. In this case, I need to go directly to the datastore because those objects in memory won’t reflect my changes just yet.

Asserting things worked!
<span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<code class='ruby'><span class='line'><span class="n">member</span> <span class="o">=</span> <span class="no">Member</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">member</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span><span class='line'>
</span><span class='line'><span class="n">assert_equal</span> <span class="mi">2</span><span class="p">,</span> <span class="n">member</span><span class="o">.</span><span class="n">member_app_roles</span><span class="o">.</span><span class="n">size</span>
</span><span class='line'><span class="n">member</span><span class="o">.</span><span class="n">member_app_roles</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">app_role</span><span class="o">|</span>
</span><span class='line'>  <span class="n">assert_equal</span> <span class="s1">'admin'</span><span class="p">,</span> <span class="n">app_role</span><span class="o">.</span><span class="n">role</span>
</span><span class='line'>  <span class="n">assert_equal</span> <span class="n">member</span><span class="p">,</span> <span class="n">app_role</span><span class="o">.</span><span class="n">member</span>
</span><span class='line'>  <span class="n">assert_not_nil</span> <span class="n">app_role</span><span class="o">.</span><span class="n">app</span>
</span><span class='line'><span class="k">end</span>

As a benefit of thinking through how to test a migration, you end up unveiling how you’d construct the migration’s down method. That is, in my case, to roll back, all I need to do is blow away the member_app_roles collection!

As you can see, testing migrations isn’t terribly difficult (probably a good reason why I haven’t found too much information about it); the key aspect is the logic that is applied to verify your migration actually worked. It should be noted that while I executed this test in the context of a document oriented database, you could certainly do the same in a relational database. For example, by dropping columns, tables, etc before running a migration. Either way, testing a migration is a snap. Can you dig it?

Notice to our Readers
We're now using social media to take your comments and feedback. Learn more about this here.