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.