Thursday, February 11, 2010

Sinatra 1.0 is a Little Too Good and an Old Rack::Test Problem Solved

‹prev | My Chain | next›

Tonight I am giving Sinatra 1.0 a try. For the first part of my new chain, I planned to upgrade EEE Cooks to CouchDB 0.10, couchdb-lucene 0.5, and Sinatra 1.0.

I tackled CouchDB and couchdb-lucene first because I expected them to be the riskiest (most likely to fail, require significant changes or just take really long). It is always best to get risky things out of the way early. Leaving them to the end invites wasted effort.

Sinatra 1.0 is not quite out yet, but the pre-release is available. To get that installed, I use the pre-release feature of rubygems 1.3.5 / gemcutter:
cstrom@whitefall:~/repos/eee-code$ gem install --pre sinatra
WARNING: Installing to ~/.gem since /var/lib/gems/1.8 and
/var/lib/gems/1.8/bin aren't both writable.
Successfully installed sinatra-1.0.a
1 gem installed
To try it out, I run my Cucumber scenarios:
cstrom@whitefall:~/repos/eee-code$ cucumber
...

39 scenarios (1 pending, 38 passed)
344 steps (1 pending, 343 passed)
2m29.522s
Dang it! It just worked. So what now? I really thought that would take up more time. Stupid backward compatibility.

After fixing a few stray unit tests / specs, I decide now might be a good time to revisit a problem that I experienced with Rack::Test last year. Specifically, I could not figure out how to stub an instance of a Rack application. Maybe updated versions of Rack and Rack::Test will get me past this.

Rack applications create normal ruby instances. Those instances must respond to call with a standard Rack response. There is no reason that these instances cannot be treated like any other object when testing. Except Rack::Test requires an "app" method, which complicates things:
def app
target_app = mock("Target Rack Application", :call => [200, { }, "Target app"])
Rack::ThumbNailer.new(target_app)
end
Getting to the Rack::Thumbnailer instance in order to set expectations or stub out method calls proved beyond me.

The specific problem that I was trying to solve last year was middleware that could create thumbnail images. It was a fairly easy problem that I was able to solve, but I was not able to easily test that solution. Specifically, there were two private methods that I wanted to stub—one that pulls the images from the target application, the other that actually makes the thumbnail.

The Rack class looks something like this:
 module Rack
class ThumbNailer
DEFAULT_CACHE_DIR = '/var/cache/rack/thumbnails'

def initialize(app, options = { })
@app = app
@options = {:cache_dir => DEFAULT_CACHE_DIR}.merge(options)
end

def call(env)
req = Rack::Request.new(env)
if !req.params['thumbnail'].blank?
filename = @options[:cache_dir] + req.path_info

unless ::File.exists?(filename)
image = rack_image(@app, env)
mk_thumbnail(filename, image)

end

thumbnail = ::File.new(filename).read
[200, { }, thumbnail]
else
@app.call(env)
end
end

private
def rack_image(app, env)
# Code to extract the image from the target application (e.g. Sinatra)
end


def mk_thumbnail(filename, image_data)
# Code to make thumbnails
end
end
end
When testing, I mostly do not care what the rack_image and mk_thumbnail private methods are doing. They are small and easily testable, but most of my tests do not need to actually perform those actions—it is enough simply that they are called. Hence the need to stub them out when testing other things (e.g. that thumbnails are not generated if they are already cached).

It turns out to be fairly trivial to accomplish this. In the app method of the spec, I memoize the rack application:
def app
target_app = mock("Target Rack Application", :call => [200, { }, "Target app"])

@app ||= Rack::ThumbNailer.new(target_app)
end
That allows me to stub methods or set expectations on my rack instance in RSpec examples:
      it "should generate a thumbnail" do
app.
should_receive(:mk_thumbnail).
with("/var/cache/rack/thumbnails/foo.jpg", "image data")
get "/foo.jpg", :thumbnail => 1
end
Last time around I had to hack something together with class methods. Being able to set expectations directly on my Rack object makes for much clearer intent.

I am not sure if I tried memoizing like this when I first attempted this or if this is only possible with more recent versions of the various gems involved. This seems like something that should have worked even back then. It also seems like something I would have tried. I am not sure if I can recreate my old setup to verify this. I will not bother though, I am content to to know that I can do this now and in the future.

Day #11

3 comments:

  1. Hi Chris,

    Thanks for this article.
    This is off topic, but what software do you use to post your code hight line. Can you share it with me?

    Thanks,
    Luan

    ReplyDelete
  2. Luan: I use http://code.google.com/p/google-code-prettify/. You can view the page source of my blog to see how I get it to work on blogspot.

    You might also try embedding gists from github. Those work nicely for most folks.

    ReplyDelete
  3. Thanks a lot for your quick reply.

    ReplyDelete