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

5 comments:

  1. nice post dude.

    But I have one doubt. Is let() and before :all are same or similar. Could you clarify my doubt?

    ReplyDelete
  2. I saw on a SO post that you commented on that another commenter said that they didn't prefer this approach because they didn't know where `user` came from, and I tend to agree. However, I do like the idea of moving all of the common setups out of the specs (I do something similar by creating custom Faker modules for each of my models). I think you could make this approach less mysterious by adding another submodule called Users that would contain your let() setup and any other user-specific setup code. Then in your spec you would include include SpecHelpers::Application::Users. This would make it obvious that you are loading up user-related code, making the call to `user` less ambiguous in scope.

    ReplyDelete
  3. I'm using rails 3.1 and it's not ActiveSupport::Concerns it's ActiveSupport::Concern with no 's'

    ReplyDelete
  4. @Spooner I made a mistake. It's ActiveSupport::Concern with no 's' in 3.0 as well. Thanks for catching that.

    ReplyDelete
  5. First of all in your second example you still use @users in the it block which I think is a typo.

    My question is in the later examples do the users get destroyed as is done by the after(:all) in the first two examples? If so can you explain how. If not is that a bug or is it not necessary?

    ReplyDelete