Squirrel and the custom nut matcher

By Nathan Donaldson in Development on September 11, 2009

Image 0484 full 2x

I recently found squirrel, and I wanted to use it for a project we’re working on to simplify some complex finder statements. Squirrel allows turning something like this:

Task.find(:all,
  :conditions => [
    'active = ? and (updated_at > cache_version or cache_version IS NULL)', true
  ]
)

into:

Task.find(:all) do
  active == true
  any do
    updated_at > cache_version
    cache_version.nil?
  end
end

The problem is testing

Then I ran into a serious problem – how to test this piece of code using rspec? Here was my first attempt:

it 'should find all active tasks where updated_at is greater than cache_version or cache_version is null' do
  Task.should_receive(:find).with(:all).and_return(@tasks)
  Task.update_cache
end

This doesn’t test the search conditions at all. So I moved on to yield:

Task.should_receive(:find).with(:all).and_yield
 received unexpected message "active"

Then I started adding in the expectations:

Task.should_receive(:active)
Task.should_receive(:find).with(:all).and_yield

But how do I know that active is being compared to true? Now I’d have to use a mock to do that:

mock_active = mock(:active)
mock_active.should_receive(:==).with(true)
Task.should_receive(:active).and_return(mock_active)
Task.should_receive(:find).with(:all).and_yield

As you can see, this is getting quite painful. So it was time to abstract this out into something more meaningful. I created a class called FindWithSquirrel:

class FindWithSquirrel
  include Spec::Matchers

  def initialize(klass, expected)
    @klass = klass
    @expected = expected
  end

  def verify
  end
end

And I added a way to gain access to the class:

class Class
  def should_receive_squirrel_find_with(expected)
    FindWithSquirrel.new(self, expected)
  end
end

Now the new class needs to extend ActiveRecord to override find and record what happens:

class FindWithSquirrel
  def initialize(klass, expected)
    ...
    extend
  end  

  def extend
    @klass.class_eval %Q{
      def self.find_with_finds
        @find_with_finds ||= []
      end

      def self.find_with_find_with(*args, &blk)
        find_with_finds << find_without_find_with(:query, &blk).to_find_conditions
        find_without_find_with(*args, &blk)
      end

      class << self
        alias_method :find_without_find_with, :find
        alias_method :find, :find_with_find_with
      end
    }
  end
end

So now I’ve got a variable on the model class holding an array of generated conditions. I can fill in the verify method:

class FindWithSquirrel
  def finds
    @klass.find_with_finds
  end

  def verify
    finds.should include(@expected)
  end
end

That’s all fine, but now I need to make rspec actually call my verify method. I can do that by reusing the way rspec mocks work. I can do that by adding my class instances to the same array that rspec adds it’s mock expectations:

class FindWithSquirrel
  def initialize(klass, expected)
    ...
    $rspec_mocks.add(self) unless $rspec_mocks.nil?
  end
end

Now the rspec mock framework is expecting to call the methods ‘rspec_verify’ and ‘rspec_reset’, so:

class FindWithSquirrel
  alias_method :rspec_verify :verify
  def rspec_reset
  end
end

Now I can run this spec, and it works great. But it seems to break all the subsequent specs that also use the find function. I have to flesh out that reset function a little. Remember my earlier extend function – I need to remove my ActiveRecord extensions:

class FindWithSquirrel
  def unextend
    @klass.class_eval %Q{
      class << self
        alias_method :find, :find_without_find_with
      end
    }
  end

  alias_method :rspec_reset, :unextend
end

So what does my spec look like?

it 'should find all active tasks where updated_at is greater than cache_version or cache_version is null' do
  Task.should_receive_squirrel_find_with(
    ["(tasks.active = ? AND (tasks.updated_at > tasks.cache_version OR tasks.cache_version IS NULL))", true]
  )

  Task.update_cache
end

So the class is testing that squirrel is outputting the correct conditions. You could argue that squirrel’s own tests fulfill this testing need, but this class gives a good jumping point to testing that squirrel is receiving the correct parameters. The full file can be found here. Just include it from your spec_helper.