Is this your first visit? You may want to subscribe to the feed.

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.

Code: bdd, cucumber, rails, search, sphinx Jun 01, 2009 ● updated Jun 25, 2009 13 comments

13 comments

  1. 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?

    Joe Van Dyk Joe Van Dyk June 02, 2009 at 12:28 PM
  2. 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.

    Brandon Brandon June 02, 2009 at 01:44 PM
  3. 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?

    Matt Matt June 03, 2009 at 07:22 PM
  4. 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.

    Brandon Brandon June 05, 2009 at 09:20 AM
  5. Thanks! I actually had to run an index manually before it would start or stop my test sphinx. Anyway, got it working. :)

    Matt Matt June 08, 2009 at 07:41 PM
  6. 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!

    schlick schlick June 11, 2009 at 09:40 AM
  7. 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.

    Brandon Brandon June 12, 2009 at 10:06 PM
  8. Thank you for this post! This has helped us finally provide coverage for the only part of our application that lacked cucumber features.

    Tim Riley Tim Riley June 16, 2009 at 08:43 PM
  9. Yep, it really works now. Excellent work!

    schlick schlick June 19, 2009 at 08:47 AM
  10. 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.
    Erik Ostrom Erik Ostrom June 23, 2009 at 06:38 PM
  11. 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
    
    schlick schlick June 25, 2009 at 11:39 PM
  12. Great!

    Thank you very much.

    bmsatierf bmsatierf September 08, 2009 at 01:02 PM
  13. 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
    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

    Guillermo Guillermo September 29, 2009 at 06:44 AM

Speak your mind:

*

*


* I hate spam and will never sell or publish your email address.

(You may use textile in your comments.)

Subscribe

Browse by Tag