Is this your first visit? You may want to subscribe to the feed.

Making RSpec concise

A common criticism of RSpec is that it is very verbose. I don’t necessarily agree (or care), but I thought it would be fun to see how concise I could make my specs.

Here are some simple specs from a client project:

describe Company do
  before do
    @company = Company.new
  end

  it "should have many classifications" do
    @company.should have_many(:classifications)
  end

  it "should have many industries through companies" do
    @company.should have_many(:industries, :through => :classifications)
  end

  it "should have many locations" do
    @company.should have_many(:locations)
  end

  it "should have many leads" do
    @company.should have_many(:leads)
  end

  it "should have many jobs" do
    @company.should have_many(:jobs)
  end

  it "should have many notes" do
    @company.should have_many(:notes)
  end

  it "should have many phones ordered by phone type position" do
    @company.should have_many(:phones, :as => :phonable,
      :include => :phone_type, :order => 'phone_types.position')
  end

  it "should have many events" do
    @company.should have_many(:events)
  end

  it "should have many interests" do
    @company.should have_many(:interests)
  end

  it "should belong_to an exchange" do
    @company.should belong_to(:exchange)
  end

  it 'should belong_to a primary_contact' do
    @company.should belong_to(:primary_contact, :class_name => 'Job')
  end

  it 'should have many articles' do
    @company.should have_many(:articles,
      :order => 'articles.date DESC, articles.created_at DESC')
  end
end

These specs check the declared associations on our company model using some custom matchers. They are not very complicated, but are somewhat repetitive. Each example has a description that is basically a duplication of the implementation.

Step 1: remove the description

For a while now, RSpec has had the ability for matchers to be self describing. If you don’t pass a block to #it, it uses the description provided by the matcher.

it do
  @company.should have_many(:jobs)
end

When that spec is run, it gives the output “should have a has_many association called :jobs”. Depending on what you’re speccing, the built in description isn’t always clear, but in this case it’s great.

See #simple_matcher if you want to create custom matchers with useful error messages.

Step 2: remove the subject

So the duplication within each example is gone, but if you look at the full spec above, each example calls @company.should. Accessing an instance variable isn’t what I would consider “duplication”, but thanks to a nifty new feature added to RSpec today, it’s now unnecessary noise. We can simply call #should within our example, and it will use a new instance of the described type as the “subject”.

describe Company do
  it do
    should have_many(:jobs)
  end
end

You can customize the subject if you don’t simply want a new instance.

describe Company, 'validations' do
  subject { Company.new(valid_company_attributes) }

  it do
    should be_valid
  end
end

Note: As David Chelimsky points out in the comments, this is not released yet and is subject to change.

Step 3: One-liner

Lastly, we can use the one line block:

describe Company do
  it { should have_many(:classifications) }
  it { should have_many(:events) }
  it { should have_many(:interests) }
  it { should have_many(:jobs) }
  it { should have_many(:leads) }
  it { should have_many(:locations) }
  it { should have_many(:notes) }
  it { should have_many(:articles, :order => 'articles.date DESC, articles.created_at DESC') }
  it { should have_many(:industries, :through => :classifications) }
  it { should have_many(:phones, :as => :phonable, :include => :phone_type, :order => 'phone_types.position') }
  it { should belong_to(:exchange) }
  it { should belong_to(:primary_contact, :class_name => 'Job') }
end

That’s pretty sexy. I wasn’t able to do this with all of the specs in my app, but it worked with quite a few of them.

Code: rspec, ruby Nov 09, 2008 ● updated Nov 11, 2008 9 comments

9 comments

  1. Brandon, this is fantastic, I can definately see usage for the subject block in my specs.

    I was wondering though, do you know if the subject block is executed once before each “it” block, similar to a before() block, or does it execute once for the whole describe block and cache the results? I’m hoping the later because often in my specs I want to run the method only one time and then use the “it” blocks to test the return values, and object state changes.

    It’s especially important when there’s DB access involved because you’re sort of penalized for fine grained specs.

    Dan Kubb Dan Kubb November 10, 2008 at 12:31 AM
  2. I just tried edge Rspec and it appears that subject block is executed once for each it block.

    It also appears as if the subject block doesn’t have access to the ivars you define in your before block. That means you can’t setup an object in a known state, use that object in the subject block and check the object state in the it blocks. It makes it hard to write stuff like: “it { should equal(@object) }” or “it { should_not change(@object, :count) }” for example.

    Dan Kubb Dan Kubb November 10, 2008 at 03:35 AM
  3. This refactoring looks very nice.

    Off topic, what I really like is the theme for your syntax highlighting! :)

    Dr Nic Dr Nic November 10, 2008 at 04:37 AM
  4. Looks like shoulda :)

    bryanl bryanl November 10, 2008 at 07:12 AM
  5. Just an FYI – this is NOT yet released, and not yet complete. The syntax is subject to change (pun intended) as I’m not 100% sold this is the right word yet. The feature will be released, but the word “subject” might become something else. Or it might not. Just keep that in mind as you choose to use this unreleased feature :)

    The motivation for adding this feature is that none of these options really work for me:

    it do
      reader.should_not have_to_ask("do it?")
    end
    it { reader.should_not have_to_ask("why am I an explicit it?")
    specify { reader.should_not have_to_ask("what's the difference between specify and it?")
    

    Being able to assign an implicit receiver to #should solves all three of those problems.

    @bryanl – the subject bit was a collaborative effort w/ Joe Ferris from thoughtbot, so, yes, looks like shoulda :)

    @Dan Kubb – #subject is only called when the example block does not have an explicit receiver for #should:

    subject  { RubyConfAttendee.new }
    it { should enjoy_hanging_with_fellow_rubyists }
    it "should spend more time hacking and less time drinking" do
      attendee = RubyConfAttendee.new(:average_beers_per_night => 7)
      attendee.should spend_more_time_hacking_and_less_time_drinking
    end
    

    In this case the first example calls the subject block and delegates #should to the result. The second example does not call the subject block.

    As for accessing what’s in before(:each), I’m not clear on what you’re trying to do, but please share your ideas with the rspec google group and/or submit feature requests to lighthouse.

    David Chelimsky David Chelimsky November 10, 2008 at 10:29 AM
  6. Brandon,

    This is def. cool; I dig that you don’t need to specify the instance var (potentially) anymore. I used to use one-liners for stuff like this:

    it { @topic.should validate_presence_of(:title) } it { @topic.should belong_to(:forum) } it { @topic.should belong_to(:creator) }

    but, obviously, taking out @topic would read a lot easier. Nice find; let’s hope this gets rolled into RSpec!

    Josh Clayton Josh Clayton November 10, 2008 at 09:47 PM
  7. David, can you tell us more about lambda?

    Breno da Mata Breno da Mata November 11, 2008 at 07:01 AM
  8. David,

    Thanks for you comments. I’ve added a note to the post.

    Brandon Brandon November 11, 2008 at 11:31 AM
  9. I finally went ahead a did my rendition of something similar but inspired by story runner. It has the potential to make pending examples disappear magically.

    • Git: http://git.oss.semanticgap.com/spec_heat.git
    • Eyes: http://git.oss.semanticgap.com/gitweb/gitweb.cgi?p=spec_heat.git;a=tree
    It just takes a handful of:
    it "has many $things" do
      it.should have_many(vars[:things].to_sym)
    end
    
    Nolan Eakins Nolan Eakins December 03, 2008 at 01:41 AM

Speak your mind:

*

*


* I hate spam and will never sell or publish your email address.

(You may use textile in your comments.)

Subscribe

Browse by Tag