Friday, June 25, 2010

Stealing Let from Rspec

Since Rspec 1.3, you could define variables in example groups that cascaded down, thus more or less eliminating the need for a before block. You do this by calling let(). By defining them with the right words, the spec code gets simplified and reads more naturally. I found it weird that it would be named after something I kept seeing around in Lisp, so I looked it up and found this. I did not understand it at all. When I cracked open the Rspec, I found an absurdly simple implementation: the Let module defines a memoized method when you call let. Since Rspec 2 breaks up examples into their own class, you get the cascading effect simply from Ruby inheritance and scoping. Dead simple. Most of the text in that file is actually documentation.

This memoized patterns happens all over the place. One example is in inherited_resources:

class ProjectsController < InheritedResources::Base
protected
def collection
@projects ||= end_of_association_chain.paginate(:page => params[:page])
end
end
I have some similar code, but since I am working on a pure web service project, I'm not actually using any assigns. But the technique is similar. inherited_resource calls out methods that are sensible defaults most of the time. To customize it, you would override one of several of the hooks, such as object, collection, end_of_association_chain. But this looks a lot nicer using the let() syntax:
class ProjectsController < InheritedResources::Base
include Let
let(:collection) { @projects ||= end_of_association_chain.paginate(:page => params[:page]) }
end
And then I dropped this into app/concerns. This was actually extracted from my (working) Rails 3 web services project:
# http://gist.github.com/453389
module Let
extend ActiveSupport::Concern

included do
extend Let::ClassMethods
end

private

def __memoized # :nodoc:
@__memoized ||= {}
end

module ClassMethods
def let(name, &block)
define_method(name) do
__memoized[name] ||= instance_eval(&block)
end
protected(name)
end
end
end
I stole this from Rspec 2, then ripped out the documentation and refactored it using ActiveSupport::Concern. I added a line to make the let() bindings protected so it won't show up as an action for the controller. This is probably the behavior you'd want in non-controller anyways.

The neatest thing I had found about let() in Rspec was easily making an implicit DSL inside my spec. The fact that I can carry this over to my controller is simply too cool not to share.

3 comments:

  1. Perhaps you would be interested in
    http://github.com/voxdolo/decent_exposure <--

    Good on you for discovering this yourself though :)

    ReplyDelete
  2. How is replacing "protected" with "include Let", "def collection" with "let(:collection) { " and "end" with "}" a lot nicer?

    ReplyDelete
  3. @rbxbx I've been using inherited_resources before Rails 2 but it's getting crufty. I'll check out decent_exposure.

    Does the expose declaration do an "assign"? Since writing this article, I've been using "assign", but as some of the other folks in the ATLRUG meetup have pointed out to me, it gets confusing with "#assigns".

    ReplyDelete