opensoul.org

Cucumber scenarios that depend on Sphinx

I love writing apps that make heavy use of search indexes, but testing them can be a bit of a pain. Here is how I got ThinkingSphinx to play nice with Cucumber.

Here is the relevant part of what I put in features/support/env.rb:

# Cucumber::Rails.use_transactional_fixtures

# http://github.com/bmabey/database_cleaner
require 'database_cleaner'
DatabaseCleaner.strategy = :truncation
Before do
  DatabaseCleaner.clean
end

ts = ThinkingSphinx::Configuration.instance
ts.build
FileUtils.mkdir_p ts.searchd_file_path
ts.controller.index
ts.controller.start
at_exit do
  ts.controller.stop
end
ThinkingSphinx.deltas_enabled = true
ThinkingSphinx.updates_enabled = true
ThinkingSphinx.suppress_delta_output = true

# Re-generate the index before each Scenario
Before do
  ts.controller.index
end

What’s going on here?

Start by commenting out the line about using transactional fixtures in env.rb. Using transactional fixtures will run each scenario inside of a transaction and roll it back at the end of the scenario to revert the database state. Thinking Sphinx uses an after_commit callback for kicking off the delta indexing, but the callback never gets run when transactional fixtures are enabled because the entire scenario is run inside of a big transaction.

Once we’ve disabled transactional fixtures, our test database will start to fill up, likely causing some problems. So we need to add a Before block that clears out the database before each scenario. I’m using database_cleaner, which gives you some different strategies for cleaning the database. Alternatively, the brute-force solution is just to reload the schema before each scenario, but this is slower than truncating the data.

Before do
  ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
  ActiveRecord::Schema.verbose = false
  load "#{RAILS_ROOT}/db/schema.rb"
end

Next, we start Sphinx when env.rb is loaded, and shut it down when the Ruby process exists. We also enable deltas and updates, which are disabled by default in test mode. Finally, we define a Before block that updates the index before each scenario so we don’t end up with a stale index.

Putting it all together

I’m using Sphinx’s delayed delta support, so whenever I update records, I need to have delayed_job process jobs. Instead of trying to get delayed_job to run in the background, I took the easy way out and defined a step: “When the system processes jobs”.

Scenario: Posting a new listing
  Given I am logged in as "MovinMan"
  When I create a new listing titled "Lots of Boxes" near "49423"
  And the system processes jobs
  And I browse listings near "49423"
  Then I can see a listing titled "Lots of Boxes"

Which is just implemented as:

When 'the system processes jobs' do
  Delayed::Job.work_off
end

If you’re just using the default deltas, and not delayed deltas, then you can update the index like this:

When /^the system updates the index$/ do
  MyModel.sphinx_indexes.first.delta_object.index(MyModel)
end

I hope that helps. Post your suggestions in the comments for improving this.

bdd, cucumber, rails, search, and sphinx June 01, 2009

14 Comments

  1. Joe Van Dyk Joe Van Dyk June 2, 2009

    The database_cleaner link is broken.

    Would it be possible to put that code into ThinkingSphinx itself? Perhaps it could run whenever it detects that cucumber is running?

  2. Brandon Brandon June 2, 2009

    Joe, thanks, I fixed the link. I’ll leave it up to the maintainer of Thinking Sphinx to decide if he wants to pull any of it.

  3. Matt Matt June 3, 2009

    Thanks for the write up, this is something I’ve been trying to do for a while.

    Unfortunately it says “Failed to start searchd daemon.” whenever I try to run my features. Did you have to change your sphinx config at all? Any other ideas?

  4. Brandon Brandon June 5, 2009

    Matt, Sorry, in the process of getting this to work, I must have run rake thinking_sphinx:configure RAILS_ENV=test, which would have generated the test config. I deleted mine locally and I get the same error. To fix it, add the following line in env.rb right before calling #start:

    ThinkingSphinx::Configuration.instance.build
    

    I’ve updated the post to include this.

  5. Matt Matt June 8, 2009

    Thanks! I actually had to run an index manually before it would start or stop my test sphinx. Anyway, got it working. :)

  6. schlick schlick June 11, 2009

    Yep, same as Matt. When no db/sphinx/test directory exists beforehand, running the cucumber tests will fail. I had to manually perform rake ts:index RAILS_ENV=test in order for this directory to be created and populated. I would’ve thought ThinkingSphinx::Configuration.instance.build would do it but it doesn’t appear to. Still, thanks for the great blog post – it was the solution I’ve been looking for!

  7. Brandon Brandon June 12, 2009

    I’ve updated the post again. Now it really works without already having the test index generated. Let me know if you have any other issues.

  8. Tim Riley Tim Riley June 16, 2009

    Thank you for this post! This has helped us finally provide coverage for the only part of our application that lacked cucumber features.

  9. schlick schlick June 19, 2009

    Yep, it really works now. Excellent work!

  10. Erik Ostrom Erik Ostrom June 23, 2009

    For my cucumber features I’ve been using fixtures, loaded from env.rb, and transactional fixtures to reset them after each scenario. Working from your post, I disabled transactional fixtures, and replaced them with DatabaseCleaner’s truncation. When I ran the scenarios, my fixtures were all gone!

    I think this is because, well, I was deleting them before each scenario. From the DatabaseCleaner README, it seems the preferred approach is to clean after each scenario:

    require ‘database_cleaner’
    DatabaseCleaner.strategy = :truncation
    Before do
    DatabaseCleaner.start
    end
    After do
    DatabaseCleaner.clean
    end

    Clean-before will work with other fixture strategies, but I don’t think there’s a down side to clean-after.

  11. schlick schlick June 25, 2009

    Alternatively, you can keep the database cleaning at the start of each scenario and simply perform a final clean at the end of all your tests using:


    at_exit do
    DatabaseCleaner.clean
    end

  12. bmsatierf bmsatierf September 8, 2009

    Great!

    Thank you very much.

  13. Guillermo Guillermo September 29, 2009

    I don’t disable transactions, so i write a method to commit the status before index:


    Given /^the search index was generated$/ do
    without_transactions do
    TS.controller.index
    end
    end

    without_transactions method just commit current transactions, yield, and create transactions again:


    def without_transactions
    @__cucumber_ar_connection.open_transactions.times do
    @__cucumber_ar_connection.commit_db_transaction
    end
    yield
    @__cucumber_ar_connection.open_transactions.times do
    @__cucumber_ar_connection.begin_db_transaction
    end
    end

    Features that use “the search index was generated” step, should be tagged with sphinx, so i can just do <pre> Before('sphinx’) do
    TS.build
    FileUtils.mkdir_p TS.searchd_file_path
    TS.controller.start
    end

    After(‘@sphinx’) do
    TS.controller.stop
    reset_database!
    end

    The reset_database! use without_transaction and do something similar to database_cleaner.

    By this way, i can use transactions in all of my code. I just need to reset the database in features (or scenarios) tagged as @sphinx

  14. mylove mylove June 19, 2011

    Честно, неплохая новость

My name is Brandon Keepers. I like to build things, usually in Ruby or JavaScript. I work at GitHub and live in Holland, MI.

Popular Posts