Thinking Sphinx RSpec Matchers

How do you use RSpec to drive the design of your models that will use Thinking Sphinx for search? Say you're already using Cucumber for integration tests to verify your index builds correctly and searches return the results you expect. For your models' specs, you'll want something that is lighter but doesn't sacrifice your overall test coverage.

To achieve this, I wrote a couple of small RSpec matchers that inspect the Thinking Sphinx index object on your model to ensure that it contains the fields and attributes that you expect.

Spec::Matchers.define(:index) do |*field_names|
  description do
    "have a search index for #{field_names.join('.')}"
  end
  match do |model|
    all_fields = field_names.dup
    first_field = all_fields.pop

    model.sphinx_indexes.first.fields.select { |field|
      field.columns.length == 1 && 
        field.columns.first.__stack == all_fields.map { |s| s.to_sym } && 
        field.columns.first.__name == first_field.to_sym
    }.length == 1
  end
end

Spec::Matchers.define(:have_attribute) do |*attr_names|
  description do
    "have a search attribute for #{attr_names.join('.')}"
  end
  match do |model|
    all_attrs = attr_names.dup
    first_attr = all_attrs.pop

    model.sphinx_indexes.first.attributes.select { |attr|
      attr.columns.length == 1 && 
        attr.columns.first.__stack == all_attrs.map { |s| s.to_sym } && 
        attr.columns.first.__name == first_attr.to_sym
    }.length == 1
  end
end

Put these matchers in your spec_helper.rb or somewhere else handy, and then you can use them in your model specs:

describe Question do
  it { should index(:topic) }
  it { should have_attribute(:state) }
end

They read quite nicely in the single-line format above, and the matchers provide a readable description when you run the spec:

Question
- should have a search index for topic
- should have a search attribute for legacy_mastery

While these matchers work well for me, I feel that @sphinx_indexes@ is perhaps an object I should leave alone, and not something I can rely on having continued access to. Please leave a comment if you have any suggestions for doing this more cleanly!

What I did learn, however, is how simple it was to write custom matchers for RSpec. If you haven't tried it before, I strongly suggest you give it a go! RSpec's matcher DSL is straightforward, and the documentation has everything you need to get started.