Search By Quacking Like ActiveRecord

Mike Burns

The story we’re trying to implement here is: as a user I want to search and filter houses so I can narrow my list of results.

To make this form and controller easier to handle we can use #form_for and pretend that the search is an ActiveRecord object. This allows the form to show fields that have been filled in or any errors on the search using existing Rails infrastructure.

The trick is that we are not storing the search in a database; we are merely pretending to be an ActiveRecord object.

So we begin from the outside with a Cucumber test:

Feature: Searching and filtering houses
  Scenario: Searching for a house by keyword
    Given a house named "Glass houses" exists with a description of
      "Don't throw rocks at me"
    And a house named "Straw houses" exists with a description of
      "Don't blow me down"
    When I go to the house search page
    And I fill in "Keyword" with "rocks"
    And I press "Search"
    Then I should see "Glass houses"
    And I should not see "Straw houses"
  Scenario: Filtering a house by price
    Given a house named "Glass houses" exists that costs 25 thousand dollars
    And a house named "Straw houses" exists that costs 10 thousand dollars
    When I go to the house search page
    And I select "20-30k" from "Price range"
    And I press "Search"
    Then I should see "Glass houses"
    And I should not see "Straw houses"

From this we can write the controller test in a straight-forward manner using test spies in mocha:

class HouseSearchesTest < ActionController::TestCase
  should_route :get, '/house_searches',
    :controller => :house_searches,
    :action => :index

  context "GET to index with houses" do
    setup do
      @houses = [Factory.stub(:house), Factory.stub(:house)]
      @search =
      @params = 'the params'
      get :index, :house_search => @params

    should_render_template :index

    should "assign the houses to @houses" do
      assert_received(@search, :results) {|expects| expects.with()}
      assert_equal @houses, assigns(:houses)

    should "assign the house search to @house_search" do
      assert_received(HouseSearch, :new) {|expects| expects.with(@params)}
      assert_equal @search, assigns(:house_search)

That gives us the interface we expect HouseSearch to conform to. We’d also typically write view tests for the form stating that it has the right action and fields and a submit button, but this blog post is too long already.

First it needs to pretend to be an ActiveRecord object:

class HouseSearchTest < ActiveSupport::TestCase
  should "build a new HouseSearch with the expected params" do
    params = {'keyword' => 'some keyword',
              'price' => '20-30'}
    house_search =
    params.each do |field, value|
      assert_equal value, house_search.send(field)

  should "handle nil on #new" do
    assert_nothing_raised do
      search =

  should "produce nil on #id" do
    search =

  should "produce true on #new_record?" do
    search =
    assert search.new_record?

Then it also needs to produce results when you ask it to:

class HouseSearchTest < ActiveSupport::TestCase
  should "produce only houses with the keyword in the description when sent
    #results for such a search" do
    matching_house = Factory(:house, :description => 'foo')
    nonmatching_house = Factory(:house, :description => 'bar')
    results ='keyword' => 'fo').results
    assert_all(results) {|house| house.description =~ /fo/}

  should "produce only houses within the price range when sent #results
    for such a search" do
    matching_house = Factory(:house, :price => 10)
    nonmatching_house = Factory(:house, :price => 50)
    results ='price' => '30-100').results
    assert_all(results) {|house| 30 <= house.price && house.price <= 100}

The last requirement to make the Cucumber pass are the view tests, as I mentioned. The outcome of the view tests (the implementation) looks like this:

<% form_for @house_search do |form| %>
  <%= form.label :keyword %>
  <%= form.text_field :keyword %>
  <%= form.label :price %>
  <%= :price, [['20-30', '20-30k']] %>
  <%= form.submit 'Search' %>
<% end %>

The key to all of this is the HouseSearch class that acts like an ActiveRecord class just enough for us to use #form_for and to simplify our controller. An extension to this is support for #errors, so that you can use in your form.

class HouseSearchTest < ActiveSupport::TestCase
  should "produce an error on the :price field when sent #new with an
    invalid price range" do
    house_search = => 'unknown')
    assert_kind_of ActiveRecord::Errors, house_search.errors
    assert_not_nil house_search.errors.on(:price)

Bonus source code

Some of you may want the resulting code. This isn’t as exciting but here it is anyway:

class HouseSearchesController < ApplicationController
  def index
    @house_search =[:house_search])
    @houses = @house_search.results

class HouseSearch
  attr_accessor :keyword, :price

  def initialize(params)
    params ||= {}
    params.each do |key, value|
      self.send("#{key}=", value)

  def id

  def new_record?

  def results
    results = House.all
    results = results.within_price_range(self.price) unless self.price.blank?

  def errors
    @errors ||=
    @errors.add(:price, "must be a price range") unless self.price =~ /-/
author image
Mike Burns

Pair with one of our expert developers to level up your skills with Coaching by thoughtbot. Save time learning best practices and techniques for reducing technical debt in Ember, Ruby, Haskell, and Go in 1-on-1 sessions tailored to your goals.