home

Boost Blog

Squirrel and the custom nut matcher

Posted by jeremy on September 11th, 2009

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.

Tags: rails, rspec, ruby, tdd

This entry was posted on Friday, September 11th, 2009 at 5:01 pm and is filed under Development, Ruby on Rails. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

2 Responses to “Squirrel and the custom nut matcher”

  1. Walter McGinnis says:
    September 11, 2009 at 6:22 pm

    Is there a reason you decided against using named_scope for “active = true” aspect of the query? You could then “chain the finders” to constrain your results more.

    The “updated_at” or semantics do look like a valid use case for squirrel though. Of course, it is 5pm on a Friday when I’m saying all this, so I may be way off base.
    Walter McGinnis´s last blog – Have fun testing your application with Cucumber My ComLuv Profile

  2. jeremy says:
    September 11, 2009 at 6:27 pm

    @walter: Hi, actually the above example is contrived just to show how squirrel does AND and OR. I’m using squirrel for slightly more complex single queries.

Leave a Reply

Click here to cancel reply.

CommentLuv Enabledshow more

  • Categories

    • Agile (3)
    • assessment (2)
    • consumer trends (8)
    • Cool tools (5)
    • Curriculum (2)
    • cybersafety (1)
    • Design (6)
    • Development (14)
    • devices (2)
    • Drupal (1)
    • e-Learning (7)
    • e-learning research network (1)
    • informal learning (5)
    • inquiry (4)
    • key competencies (6)
    • learning communities (3)
    • magic and delight (5)
    • Maori achievement (2)
    • multiliteracies (1)
    • professional learning (6)
    • Publishing (3)
    • Random thoughts (3)
    • research (7)
    • Ruby on Rails (8)
    • Sarah's top ten (11)
    • Social media (7)
    • Social software: practices (5)
    • social software: tools (9)
    • software (4)
    • Software for Learning website (4)
    • student work (7)
    • teacher-learner roles (5)
    • teaching practice (9)
    • the curriculum (6)
    • transformation (10)
    • Usabilty (3)
    • Writing (1)
  • Archives

    • August 2010 (4)
    • July 2010 (6)
    • June 2010 (2)
    • April 2010 (1)
    • March 2010 (1)
    • February 2010 (1)
    • January 2010 (3)
    • December 2009 (1)
    • November 2009 (1)
    • October 2009 (4)
    • September 2009 (2)
    • August 2009 (3)
    • July 2009 (6)
    • June 2009 (3)
    • May 2009 (1)
    • April 2009 (6)
    • March 2009 (6)
    • February 2009 (11)
    • December 2008 (4)
    • November 2008 (6)
    • October 2008 (12)
    • September 2008 (7)
    • August 2008 (7)
    • July 2008 (4)
  • Boost Loves Design

    • I love Typography
    • IntuitionHQ | easy website usability
    • OMG It even has a watermark
    • Follow me on Twitter
    © Boost Limited.
    All rights reserved.
    CONTACT US
    info@boost.co.nz
    tel. (04) 939 0062
    fax. (04) 939 0063

    Level 6, 175 Victoria Street
    PO Box 11504, Wellington
    New Zealand