Software Developer

Testing Active Record-based Modules

As Rails developers, Active Record plays a very large role in our applications. Active Record models often share a lot of behavior that can be extracted into modules, which are built on top of, and rely on, Active Record behavior.

Here we will discuss strategies for testing these Active Record-based modules, and particularly, how we can isolate those tests from the surrounding application. We will be using RSpec as our testing platform in these examples.

The Setup

As a very simple example, let’s say we have a module that defines sorting behavior on our models. This module has a single method, sorted, which returns the model records, sorted by their position attribute.

module Sortable
  def sorted
    order(:position)
  end
end

This behavior is shared across several models in our application, each of which will extend the Sortable module.

class SortableClass < ActiveRecord::Base
  extend Sortable
end

How should we go about testing our Sortable module?

Sharing is Caring

One common method of testing this behavior is by using RSpec shared examples.

shared_examples 'it is sortable' do
  let!(:record_1) { described_class.create(position: 2) }
  let!(:record_2) { described_class.create(position: 1) }

  describe '.sorted' do
    it 'sorts the records by position' do
      expect(described_class.sorted).to eq [record_2, record_1]
    end
  end
end

We can then use these shared examples in each of our models that extend the Sortable module.

describe SortableClass do
  it_behaves_like 'it is sortable'
end

This is a good way to test the behavior of our module within the context of an Active Record model, and to ensure that all of our Sortable models behave as we expect.

However, there are some issues with this method: - If we are including this behavior in a lot of models, the shared examples can quickly bloat our test suite - Model validations and callbacks can complicate record creation, requiring additional complexity in our test - Does not allow us to isolate and test the module behavior directly

How can we test our module in isolation, without relying on any of our application’s existing models?

Double Down

The simplest way to handle this is to leverage RSpec mocks to stub out all of the interaction with other objects.

By utilizing object doubles and method stubbing, we could isolate our module and test that it is calling the correct Active Record methods. This is great, because we are isolating our module’s behavior both from the application models, as well as from Active Record itself.

class Sortable::MockSortableClass
  extend Sortable
end

describe Sortable::MockSortableClass do
  describe '.sorted' do
    let(:sorted_records) { double }

    it 'sorts the records by position' do
      expect(described_class).to receive(:order).with(:position)
        .and_return(sorted_records)

      expect(described_class.sorted).to eq sorted_records
    end
  end
end

But what are we actually testing here? We know that we are calling a particular method, but we have no idea if that method is producing the behavior that we want. We can’t even be sure that we are calling the method correctly, without any errors. This test just doesn’t get us very much outside of the context of Active Record.

So, we want to keep our module’s tests isolated from the rest of our application, but with real Active Record interaction and behavior. How can we accomplish this?

Here One Test, Gone The Next

We can solve this problem by creating a temporary database table, which exists only in the context of our spec.

By using Active Record’s database connection adapters, we can directly interact with the database in our tests. ActiveRecord::Base.connection will return an active connection adapter to our test database.

By using the create_table and drop_table methods on the connection adapter, we can create (and subsequently destroy) a custom database table, specifically for the purposes of our test.

In our case, those calls look like this:

ActiveRecord::Base.connection.create_table :mock_table do |t|
  t.integer :position
end

ActiveRecord::Base.connection.drop_table :mock_table

These calls can be placed in before(:all) and after(:all) blocks, respectively, to create the table immediately before our tests are run, and destroy it immediately after.

This gives us the benefit of a table that exists only in this context, tailored specifically for our needs. All we really need is our position column, so we have no extraneous columns on this table.

From here, we need to build an Active Record class on top of this table, and extend it with our Sortable module. As with our table, this class can be temporary, and only accessible within our test, meaning we can define an anonymous class at runtime:

Class.new(ActiveRecord::Base) do
  self.table_name = 'mock_table'
  extend Sortable
end

This defines a new anonymous class, inheriting from ActiveRecord::Base, on top of our temporary table, and extends it with the necessary Sortable behavior.

Now that we have our database table created, and our Active Record class defined, we can use these to directly test the behavior of our module, isolated from the rest of our application:

describe Sortable do
  let(:mock_class) { build_mock_class }
  let!(:record_1) { mock_class.create(position: 2) }
  let!(:record_2) { mock_class.create(position: 1) }

  before(:all) { create_table }
  after(:all) { drop_table }

  describe '.sorted' do
    it 'sorts the records by position' do
      expect(mock_class.sorted).to eq [record_2, record_1]
    end
  end

  def build_mock_class
    Class.new(ActiveRecord::Base) do
      self.table_name = 'mock_table'
      extend Sortable
    end
  end

  def create_table
    ActiveRecord::Base.connection.create_table :mock_table do |t|
      t.integer :position
    end
  end

  def drop_table
    ActiveRecord::Base.connection.drop_table :mock_table
  end
end

Multiplicity

Our method works quite well for our Sortable module. However, we run into a problem when we repeat this strategy for other modules. Whichever spec file is loaded first will run without issue, but the next file to be loaded will throw an ActiveRecord::UnknownAttributeError when it tries to run. What happened?

This occurs because Active Record caches table and column information on its models. The first time we use one of our temporary tables, Active Record will store that information in the cache, which will persist even after we drop and recreate the table with new columns. The new column information will not be loaded, which causes the aforementioned error.

We can solve this problem by calling reset_column_information in each of our temporary model definitions, after setting the table name. This method will clear out Active Record’s cached information for our models and tables, and reload that information on the next request. In our Sortable example, we should change our model definition to this:

Class.new(ActiveRecord::Base) do
  self.table_name = 'mock_table'
  reset_column_information
  extend Sortable
end

Gemification

What if we find ourselves re-using our Sortable code in several applications? It might be time to pull our module out into its own gem, so we can easily include it in all of our projects. Being the good developers that we are, we want our new gem to be fully tested. That’s easy, since we’ve already isolated our tests from the rest of our application, we can extract them directly into the gem!

One problem: outside of our application, we no longer have a database connection. Turns out, we may not have been as isolated as we thought.

Luckily for us, Active Record makes it easy to create an in-memory database, which will work perfectly as temporary test storage for our gem. All we need to do is add the following:

ActiveRecord::Base.establish_connection(
  adapter:  'sqlite3',
  database: ':memory:'
)

We can place this in the gem’s spec_helper.rb file, or in our individual spec files, before we create the table. We now have an in-memory SQLite* test database for our gem.

Check out Foraker’s ActiveChronology gem to see this strategy in action.

* This is a potential problem for any code that relies on a particular database implementation, but should be fine for most cases, especially if we are striving to be database-agnostic.

Fin

And we’re all set! We’ve taken our simple Active Record module, separated its tests from the rest of our application, and successfully extracted it into a completely isolated and fully tested gem.

Tweet at Foraker

Share this post!