Friday, March 6, 2009

Test CouchDB Database with Cucumber and Sinatra

‹prev | My Chain | next›

Continuing the chain, tonight I finish the spike of using Cucumber, Sinatra, and CouchDB together.

Thanks to a hint from Joseph Wilk, I already have a good idea how to work with a CouchDB test instance. Let's see if it pans out...

First I modify the spike.rb Sinatra code to use a different database by adding an environment-dependent db method:
require 'rubygems'
require 'sinatra'
require 'rest_client'
require 'json'

#####
# Use a different CouchDB instance in test mode
def db
Sinatra::Application.environment == :test ?
"http://localhost:5984/eee-test" :
"http://localhost:5984/eee-meals"
end

get '/meals/:permalink' do
data = RestClient.get "#{db}/#{params[:permalink]}"
result = JSON.parse(data)
%Q{
<h1>#{result['title']}</h1>
<p>We enjoyed this meal on #{result['date']}</p>
<p>#{result['summary']}</p>
<p>navigation and links to recipes would go here...</p>
<div>
#{result['description']}
</div>
}
end
A Before block (which is a Cucumber hook) can be added to create the test DB before each scenario (db is the method from the Sinatra app):
Before do
RestClient.put db, { }
end
Similarly, an After block can be used to tear down the DB after each scenario:
After do
RestClient.delete db
end
These blocks seem to work fine in the features/support/env.rb file, which is then:
# NOTE: This must come before the require 'webrat', otherwise
# sinatra will look in the wrong place for it's views.
require File.dirname(__FILE__) + '/../../spike'

# RSpec matchers
require 'spec/expectations'

# Webrat
require 'webrat'
Webrat.configure do |config|
config.mode = :sinatra
end

World do
session = Webrat::SinatraSession.new
session.extend(Webrat::Matchers)
session
end

Before do
RestClient.put db, { }
end

After do
RestClient.delete db
end
Lastly, I need to implement the Given step from yesterday's spike (see the discussion in the comments):
Given /^a "(.+)" meal$/ do |title|
@permalink = title.gsub(/\W/, '-')

RestClient.put "#{db}/#{@permalink}",
{ :title => title }.to_json,
:content_type => 'application/json'
end
If the Given text reads 'Given a "Breakfast Elves" meal', then the string "Breakfast Elves" will be assigned to the block's title variable. I use that to calculate a permalink (id) for the document by replacing all non-characters with a dash. RestClient puts the new document, with the appropriate title, into the DB.

By assigning the permalink to an instance variable, it is re-usable in subsequent steps, in this case the "When I view the meal permalink" step:
When /^I view the meal permalink$/ do
visit("/meals/#{@permalink}")
end
Thus, then entire steps file for my spike becomes:
Given /^a "(.+)" meal$/ do |title|
@permalink = title.gsub(/\W/, '-')

RestClient.put "#{db}/#{@permalink}",
{ :title => title }.to_json,
:content_type => 'application/json'
end

When /^I view the meal permalink$/ do
visit("/meals/#{@permalink}")
end

Then /^the title should include "(.+)"$/ do |title|
response.should have_selector("h1", :content => title)
end
Now when I run it, I get 3 passing steps without the oogy feeling about that empty Given:
Feature: See a meal

So that I can see an old meal
As a web user
I want to browse a single meal by permalink
Scenario: View Meal
Given a "Breakfast Elves" meal
When I view the meal permalink
Then the title should include "Breakfast Elves"


1 scenario
3 steps passed
All that is left is to rm -rf the spike code.

Some Final Thoughts

Creating and deleting the DB for each scenario is a coarse solution compared to using transactions to rollback test data in ActiveRecord tests. Even so, this just might be acceptable.

The scenario runs in under 0.5 seconds. That is about what the typical scenario takes in my experience with Rails.

It is not as if any structure needs to be built up in the CouchDB instance upon DB creation—that is one of the points of a document oriented database, after all.

I will have to ruminate on this, but reimplementing EEE Cooks on Sinatra might just work. Then again, it may prove hard to resist a Merb or Rails edge spike over the next couple of nights.

No comments:

Post a Comment