Sunday, July 01, 2007

RSpec "should_yield"?

While writing some specifications for the 0.2.0 version of Twitter4R (soon to be released - honestly), I realized there was no clean way to do #should_yield, so I thought I would offer my suggestion for the developers of RSpec to critique as appropriate.

Assume we have a method signature that takes a block and should yield an object to the block given (if one is given). Something like the following:


class Client
class << self
def configure(&block)
# implementation of Client.configure
end
# rest of class methods for Client class go here...
end
# rest of instance methods for Client class go here....
end

Where usage for the Client.configure method should look something like the following:

Client.configure do |conf|
conf.proxy_host = 'myproxyhost'
conf.proxy_port = 8080
conf.proxy_user = 'me'
conf.proxy_pass = 'mypass'
end

The object yielded to the block given is an instance of Config which has settable attributes: proxy_host, proxy_port, proxy_user, proxy_pass.

The question is how do we write RSpec specifications for this?

First we need to define what we want to specify for the expected behavior. My first attempt is to describe (in English) that:
  • Client.configure should
    • accept a block as argument
    • yield a Config instance to the given block

The way I did this in the Twitter4R (open source Ruby library for the Twitter REST API) project specifications was something like the following:

describe Client, ".configure" do
before(:each) do
@has_yielded = false
@block = Proc.new do |conf|
conf.is_a?(Config).should be_true
@has_yielded = true
end
end

it "should accept a block as argument" do
lambda {
Client.configure {}
}.should_not raise_error
end

it "should yield a Config object to given block" do
Client.configure(&block)
@has_yielded.should be_true
end

after(:each) do
@has_yielded, @block = nil
end
end

What I would really like to do something like the following instead:

describe Client, ".configure" do
it "should accept a block as argument" do
lambda {
Client.configure {}
}.should_not raise_error
end

it "should yield a Config object to given block" do
lambda {
Client.configure(&spec_block(1))
}.should yield_with(Config)
end
end

I haven't extended the Proc class to this point, but if/when I do I will post it to this blog and perhaps also to the RSpec mailing list.

If you have suggestions on how to improve the more manual way (the first RSpec code listing above), please do let me know. I have not yet played around with inheritable describe blocks yet, but I have hunch that would DRY up some of the code for if needed for many contexts.

Alternatively if you have suggestions on how to improve the second RSpec code listing (directly above) to be more clear, please do let me know that too. I realize the developers of RSpec don't want to have too many #should_XXX methods, so I am open to suggestions (since I also agree with that notion).

UPDATE: I created a new feature request and dialog with RSpec developers at: [#11949]

0 comments: