Friday, February 11, 2011

Agile rspec with let()

Rspec 1.3 has the least-used, yet significant feature called let().

Here's how you set up Rspec 2, for those who have not caught on to let().

describe Friendship do
before :all do
@users = (1..5).collect { Factory(:user) }
end

after :all do
@users.each { |user| user.destroy! }
end

it 'should do something' do
@users.each do |user|
user.should be_valid
end

# Something interesting with @users
end
end

You should be able to replace everything you use @variables for with let(), like this:

describe Friendship do

let(:users) { (1..5).collect { Factory(:user) } }

before :all do
users
end

after :all do
users.each { |user| user.destroy! }
end

it 'should do something' do
@users.each do |user|
user.should be_valid
end

# Something interesting with @users
end
end
I prefer not to preload everything. The tradeoff is slower specs:

describe Friendship do
let(:users) { (1..5).collect { Factory(:user) } }

it 'should do something' do
users.each do |user|
user.should be_valid
end
# Something interesting with @users
end
end
let() cleans up a lot of your code. But where it really shines comes in combination with two things: (1) rspec 2 will preload everthing under spec/support, and (2) Rails 3's secret weapon, ActiveSupport::Concern. You can factor out your let() declarations and share it across your spec, like so:
# Put this in spec/support/application.rb

module SpecHelpers
module Application
extend ActiveSupport::Concern

included do
let(:users) { (1..5).collect { Factory(:user) } }
end
end
end

# spec/friendship_spec.rb
# Rspec 2 automatically loads everything in spec/support
require 'spec_helper'

describe Friendship do
include SpecHelpers::Application

it 'should do something' do
users.each do |user|
user.should be_valid
end
# Something interesting with @users
end
end

# spec/comment_spec.rb
require 'spec_helper'

describe Comment do
include SpecHelpers::Application

it 'should do something' do
users.each do |user|
user.should be_valid
end
# Something interesting with @users
end
end

Tuesday, January 18, 2011

Scoping and Closure in Ruby 1.8


The other day, I had to wrote code in this form:
class_eval do
[:foo, :bar].each do |a|
_cache = :"@#{a}_cache"
define_method a do
_cache
end
end
end
I had to ask myself: does this really do what I expect? I know the define_method closes over the _cache variable, but is it really isolated in each iteration? Am I really referencing a different _cache on each iteration? I tried this in the Rails console:
=> [:foo, :bar]
irb(main):009:0> foo
=> :@foo_cache
irb(main):010:0> bar
=> :@bar_cache
Should this have been surprising? I suppose not.

Friday, September 10, 2010

Trick Debugging with Object#tap

Since ActiveSupport 3.0 deprecated Object#returning in favor of Object#tap, we can make use of inline debugging like this:

(1..100).to_a.map { |x|
x * 2
}.inject( 0 ) { |a, b|
a + b
}.tap { |sum| p sum }


Being used to Symbol#to_proc, this looks so awkward. I'd rather do this:

WATCH = lambda { |*args| p *args }

(1..100).to_a.map { |x|
x * 2
}.inject( 0 ) { |a, b|
a + b
}.tap(&WATCH)


Extending that idea, you can make something that'd log things to file, vial Rails logger. Or you can make use of metaprogramming:

require 'ap' # awesome_print gem
def watch(sym)
lambda { |*args| ap ( { sym => args[0] } ) }
end

(1..100).to_a.map { |x|
x * 2
}.inject( 0 ) { |a, b|
a + b
}.tap(&watch(:sum))

Tuesday, July 6, 2010

Writing ebooks with ERB

For me, the upper limit of acceptable commute time is 10 minutes, but a longer commute time does give me time to think up of things while navigating a familiar route. One such idea came from a sudden desire to write a book. Having gotten Stanza for my iPod, and having a mind that cross-links ideas together like an encyclopedia, I'd want to publish it via epub and on a MediaWiki platform. I'm getting too old for copy-and-paste editing, so having a source document written in ERB would be ideal. In fact, I even got as far as a vague notion of using Nokogiri and/or XML as some sort of intermediate representation.

Turns out, while (as usual) searching for something completely unrelated, I found that someone has already implemented this tool: http://snk.tuxfamily.org/lib/erbook/.

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.

Thursday, June 24, 2010

Untitled Haiku #1

irb(main):001:0> lisp(:code_generator).to_ruby
=> :metaprogramming

Friday, June 11, 2010

Remarkable 4.0.0.alpha4

Rails 3.0 Beta4 is out! and thanks to Louis T. we have released a alpha4 that tracks Rails 3.0 Beta4. You will need to run RSpec 2.0 alpha11 in order to run this version.

As usual, remarkable/rails does not work. I know a lot of people have asked for this. I personally don't have a need for those macros, though there are a few who looks like they are stepping up to the plate. If you want an alternative, I highly recommend looking at the Rspec let feature which lets you do much of the same thing, minus i18n. I may publish a remarkable/rack, or at least, publish a blog post on how to do it with let. If you have not already, I highly recommend looking at the Pure RSpec slides.

Changes

  • The only real change was a fix for validate_numericality_of resulting from a patch submitted for Beta4. It is Rails #4406 if you are curious.