The RSpec Book is now in beta
January 29th, 2009
I’m pleased to announce the beta release of the Pragmatic Bookshelf’s The RSpec Book: Behaviour Driven Development with RSpec, Cucumber and Friends!
It’s been a long time coming, and there’s still a lot of work to do to get to print. The beta release has 9 of the 22 chapters we have planned. Most of what remains is almost done, but not quite ready to release yet. As with all of the Pragmatic beta books, we’ll do regular updates every few weeks as we wrap up the remaining chapters and incorporate your feedback. And please do provide feedback. We want this book to serve you well!
There are six authors involved: Dave Astels, Zach Dennis, Aslak Hellesøy, Bryan Helmkamp, Dan North, and me. I’m honored to be in such good company here, with the guys who brought us BDD, to the developers and maintainers of RSpec, Cucumber and Webrat.
You can read more (and buy the book!) at http://www.pragprog.com/titles/achbd/the-rspec-book
On behalf of all the authors, I’d like to extend a special thank you to all of you who have contributed to the software and the conversation around RSpec, Cucumber, and BDD in general. RSpec would be nothing without the community that has evolved around it, so thank you, thank you, thank you!
rspec-1.1.12 is released
January 13th, 2009
We just released rspec-1.1.12. You can read the changelogs for rspec and rspec-rails for all the details, but are some changes that you should definitely know about.
Cucumber is the new Story Runner
rspec-1.1.12 is the last release that will ship with the Story Runner bundled. With the next release, we will do the one and only gem release of repo so you won’t be forced to upgrade to Cucumber Thanks to Chad Humphries for stepping in and extracting the Story Runner to a separate repo.
The Story Runner is deprecated, however, and we will not be maintaining it. If anybody wants to maintain it, you’re welcome to. Just fork the repo and have at it.
Why are we deprecating the Story Runner? Because Aslak Hellesøy’s Cucumber library kicks its ass. Easier setup means lower barrier to entry, building on treetop means great feedback with backtraces plus support for an ever-growing list of written languages. And migrating from RSpec Stories is a snap. So what are you waiting for?
rspec-1.1.12 release candidate
January 4th, 2009
I’m getting ready to do a 1.1.12 release of rspec and rspec-rails. Given the history of release-related compatibility problems, I offer you release candidate gems, which you can acquire thusly:
UPDATE: new version (1.1.11.6) fixes dependency problem w/ github gems
gem sources --add http://gems.github.com
[sudo] gem install dchelimsky-rspec -v 1.1.11.6
[sudo] gem install dchelimsky-rspec-rails -v 1.1.11.6
Release notes can be seen under Maintenance at:
- http://github.com/dchelimsky/rspec/tree/master/History.txt
- http://github.com/dchelimsky/rspec-rails/tree/master/History.txt
NOTE: This will be the last release of rspec-rails that supports rails < 2.0
If you are so inclined, please grab these gems, use them, and let me know if there are any problems.
cucumbers creating cucumbers
October 13th, 2008
I’m preparing to present a practical demonstration of rspec, cucumber and friends at Rails Summit Latin America this Thursday.
<p>I was playing with the idea of using cucumber/rspec to drive the development of a cucumber browser/editor, but I’ve decided that it ends up being a bit too meta for a conference presentation.</p>
<p>Of course, nothing is too meta for a blog post, and clearly I’ve already procrastinated a great deal if it’s just a few days before and I’m still prototyping the app, so why not take some more time away from what I should be doing and post this sillyness?</p>
<p>And with <strong>that</strong> ... enjoy!</p>
Feature: create featureSo that I can easily create new feature As a stakeholder I want to create a feature in a browser
Scenario: create feature When I create a new feature named "Eat Cheese" And I give "Eat Cheese" the narrative: So that I can be happy As a cheese-loving person I want to eat cheese And I add a scenario to "Eat Cheese" named "roquefort" And I add a step to "roquefort" with "Given I am holding my nose" And I add a step to "roquefort" with "When I eat a hunk o' roquefort" And I add a step to "roquefort" with "Then I should smile at its deliciousness"
And I save and run the "roquefort" scenario Then I should see "3 steps pending" And I should see: You can use these snippets to implement pending steps: Given /^I am holding my nose$/ do end When /^I eat a hunk o' roquefort$/ do end Then /^I should smile at its deliciousness$/ do end
DISCLAIMER: This also assumes some support for multi-line steps that has not yet been implemented and may not be supported as depicted here. So please don’t try this at home.
Windy City Rails Conf
September 23rd, 2008
This past weekend, I presented on BDD and RSpec at the first annual Windy City Rails Conf. As is typical, I ran out of time and didn’t get to get down to details. My next talks are starting w/ code examples!
<p>Until then, here are the <a href="/assets/2008/9/23/BehaviourDrivenRails.WindyCityRails.2008.pdf">slides from the presentation</a>.</p>
Cucumber
September 22nd, 2008
Aslak Hellesøy’s Cucumber library will be replacing RSpec’s Story Runner after the RSpec 1.1.5 release (coming soon).
<p>Cucumber is a bottom up re-write of the Story Runner, and features a grammar parser using <a href="http://treetop.rubyforge.org/">Treetop</a>. When I first started working on support for plain text stories, I chose to roll my own parsing rather than writing a grammar for a number of reasons, but it turns out that we get some great benefits from it.</p>
<h3>Cucumber supports multiple spoken languages:</h3>
<p>So now you can say:</p>
Funcionalidade: Adição Para evitar erros bobos Como um péssimo matemático Eu quero saber como somar dois númerosCenário: Adicionar dois números Dado que eu digitei 50 na calculadora E que eu digitei 70 na calculadora Quando eu aperto o botão de soma Então o resultado na calculadora deve ser 120
Que legal! (How cool is that?)
<p>There are already several languages supported, and adding new ones is fairly trivial, so we’ll likely support adding your own languages after some time.</p>
<h3>Improved backtraces</h3>
<p>Cucumber includes line numbers from the plain text Feature files, making it much, much easier to understand failures. (NOTE: we’re calling them Features now instead of Stories – look for another post on that subject soon)</p>
<h3>Simpler configuration</h3>
<p>Cucumber eliminates steps_for and using_steps_for. Simply define steps using the Given, When and Then methods:</p>
features/steps/accounts.rb
Given /I have \$(\d+) in my (.*) account/ do |dollars, account_type| ... end
Now require the files with the step definitions you need:
cucumber -r features/steps/accounts.rb features/transfer_money
… and you’re off. For most cases you don’t even need that granularity, you can just say …
<pre><code>cucumber features</code></pre>
<p>... and cucumber will require any .rb files it finds in the features directory before running the feature files.</p>
<h3>Fewer surprises</h3>
<p>When RSpec’s Story Runner finds more than one step definition that can handle a step, the first one it finds wins. This can lead to some painful debugging sessions.</p>
<p>When Cucumber finds more than one step definition that can handle a step, you get an error telling you which step definitions are competing, including their location (file and line number), so you can easily see and resolve the conflict.</p>
<h3>What this means for you if you’re already using Story Runner</h3>
<p>Cucumber is only a few months old and is nearly feature compatible with RSpec’s Story Runner and already adds a lot of powerful new features. Aslak has converted many, many stories to cucumber features, and is <a href="http://github.com/aslakhellesoy/cucumber/wikis/migration-from-rspec-stories">posting about his experiences</a> and refining the process as he goes. By the time we release cucumber as the official scenario runner, the migration path will be well documented and inexpensive.</p>
<p>As for a time frame, that’s difficult to say. We’ve been promising the 1.1.5 release for some time and for one reason or another it keeps getting pushed back. We’ll likely wait for the rails 2.2 release and make sure that it is compatible. Rumor has it that is coming soon, but it was coming soon several weeks ago as well, so we’ll have to wait and see.</p>
<p>In the mean time, we are freezing development on Story Runner so that we can focus on Cucumber development. Before we officially release cucumber as part of rspec, we’ll create a separate project up on github for just the story runner (likely named rspec-stories) so the code will be available for teams that want to continue to use and/or maintain it.</p>
<p>I’ll follow up here and on the <a href="http://rubyforge.org/mailman/listinfo/rspec-users">rspec-users mailing list</a> (which is mirrored by the <a href="http://groups.google.com/group/rspec">rspec google group</a>) as things progress.</p>
How I got started programming
July 1st, 2008
Tagged by Bryan Helmkamp.
How old were you when you started programming?
Mid 20’s for a minute. Then mid 30’s for real.
How did you get started programming?
There were two starts. First, a tiny bit of background.
My father was a programmer for a minute (great stories about tripping on the way to deliver his final project in school, punch cards flying through the air in every-which direction), and my step father worked for Chemical Bank’s Data Processing department during my childhood. So I had some exposure to the results of programming as a kid, but had never really looked at any code. I was too busy shuffling cards and making coins disappear (my middle name, dare I admit it, is Arthur).
At 23, I was getting started as a musician (my 2nd career) and sought a non-music gig to get me through. A friend hooked me up as the copy-room attendent at an engineering firm. Apparently, this job had been occupied by people even less responsible that I was (a tough thing to be next to me at that age) because everyone was shocked when a couple of weeks in I was done each day by early afternoon and offering to help in other areas the rest of the day.
That’s when the guy who ran IT gave me the keys to the castle: a user account on their DIGITAL network and access to the giant BASIC manuals. I read through them in my spare time and wrote my first program: a musical scale generator. It went something like this:
$ What root? D $ What quality? minor $ D E F G A Bb C D $ What root?
Dig the simple UI! I got it to handle major, natural, harmonic and melodic minor scales and all the modes of each. I was quite proud.
Eventually I made the ill-fated, romantic decision to earn all of my pennies from musical endeavors, so I quit at the engineering firm, turning in my keys to the castle, and started teaching guitar to fill that hole in my pocket.
Skip ahead 10 or 15 years. By then (1998) had a degree in music and was earning a living playing, teaching and arranging music. You’ve heard this story before. All the money came from gigs that involved tuxedos or commercials, and I got through those gigs to sponsor my habit of playing jazz and rock in clubs for beer money.
During this time, I had acquired an Apple Performa (remember?), had figured out how to make simple html websites and was helping some musician friends with theirs. I really, really enjoyed this, and my girlfriend noticed that I smiled when I made a website or played Windows (unfortunately titled, but beautiful tune by Chick Corea) but I frowned when I played Celebration. She suggested that I should consider taking a programming course and trying to get a part time job somewhere, thus sponsoring the smiley parts of my music habit with something that also made me smile.
This was the late 90’s, smack in the middle of the bubble, and it seemed like a perfectly reasonable idea. Little did I know that 10 years later I’d have hung my guitars in the closet to work on OSS. They’re still hanging there. This makes me sad. But I haven’t sold them yet, so there is still hope.
Anyhow, I took a couple of classes at a local community college, got a job working in the school’s IT department, and here we are 10 years later.
What was your first language?
BASIC
What was the first real program you wrote?
Depends on your definition of real. I wrote an applet when I was in school that let you try to solve a brain teaser. The first thing I got paid money to write that actually got used by people to make other money was an online training course for hospital administrators to learn how to fill out government forms properly.
What languages have you used since you started programming?
In no particular order: Ruby, Javascript, Python, Java, C#, PHP, Cold Fusion (OK, that is in a particular order).
What was your first professional programming gig?
The aforementioned hospital admin training course.
If there is one thing you learned along the way that you would tell new developers, what would it be?
Best practices are the serpent. They have the potential to be useful, but they are only useful in a very limited set of contexts, and they are applied just as often (if not more often) in the wrong ones as the right ones. Take the blinders off.
What’s the most fun you’ve ever had programming?
Working on an accounting-support system for a non-profit. This was about a year long project. We were doing (mostly) XP, pairing, and working closely with the people who actually used the software. The team included Micah Martin, Paul Pagel and Craig Demyanovich, all three of whom contributed to pairing sessions filled with a great balance of joy and head-butting. I learned a ton and had a great time doing it.
Aside from the fun we had programming, there were a lot of fun non-programming moments as well. I think my favorite on that gig was one morning when we were all arriving at the office. We had started greeting each other in different languages, Micah in French, me in Portuguese, and Christine (a woman who was not on the team, but worked close by) in Italian. Craig had joined us for the first time since we had been doing this, and he caught on rather quickly:
Me: bom dia, gente!
Micah: bon jour!
Christine: buongiorno!
Craig: puts “hello”
Another great moment on that project was sitting down with a user and watching her do her data entry job. She was really, really, really fast. But there was this one part in the process where she would pause for each entry. She wasn’t even aware of it, but recognized the problem when I brought it to her attention. So we made a small change to the UI and got rid of this small, but meaningful impediment and her life got just a little better. Very satisfying.
Up Next
Here are some folks who don’t seem to have been tagged yet. Let’s see if they’ll play …
new controller examples
July 1st, 2008
There’s been a lot of discussion about clarity over DRY lately. This is something that I’ve been espousing for some time, but recent posts by Jay Fields, Mikel Lindsaar and Dan North have gotten me thinking about it again with more focus.
With this in mind, I’ve been refining the examples generated for restful controllers when you run script/generate rspec_scaffold with the rspec-rails plugin. I’ve got them now where I’m pretty happy with them, but I’m curious to hear what you think. I’m not going to tell you what I changed or what to look for, I’m just going to ask you to look it over and post your comments.
There are two listings: the generated code and the output you get from running the examples. Thanks in advance for any feedback.
script/generate rspec_scaffold account
require File.expand_path(File.dirname(FILE) + '/../spec_helper')describe AccountsController do
def mock_account(stubs={}) stubs = { :save => true, :update_attributes => true, :destroy => true, :to_xml => '' }.merge(stubs) @mock_account ||= mock_model(Account, stubs) end
describe "responding to GET /accounts" do
it "should succeed" do Account.stub!(:find) get :index response.should be_success end it "should render the 'index' template" do Account.stub!(:find) get :index response.should render_template('index') end it "should find all accounts" do Account.should_receive(:find).with(:all) get :index end it "should assign the found accounts for the view" do Account.should_receive(:find).and_return([mock_account]) get :index assigns[:accounts].should == [mock_account] endend
describe “responding to GET /accounts.xml” do
before(:each) do request.env["HTTP_ACCEPT"] = "application/xml" end it "should succeed" do Account.stub!(:find).and_return([]) get :index response.should be_success end it "should find all accounts" do Account.should_receive(:find).with(:all).and_return([]) get :index end it "should render the found accounts as xml" do Account.should_receive(:find).and_return(accounts = mock("Array of Accounts")) accounts.should_receive(:to_xml).and_return("generated XML") get :index response.body.should == "generated XML" endend
describe “responding to GET /accounts/1″ do
it "should succeed" do Account.stub!(:find) get :show, :id => "1" response.should be_success end it "should render the 'show' template" do Account.stub!(:find) get :show, :id => "1" response.should render_template('show') end it "should find the requested account" do Account.should_receive(:find).with("37") get :show, :id => "37" end it "should assign the found account for the view" do Account.should_receive(:find).and_return(mock_account) get :show, :id => "1" assigns[:account].should equal(mock_account) endend
describe “responding to GET /accounts/1.xml” do
before(:each) do request.env["HTTP_ACCEPT"] = "application/xml" end it "should succeed" do Account.stub!(:find).and_return(mock_account) get :show, :id => "1" response.should be_success end it "should find the account requested" do Account.should_receive(:find).with("37").and_return(mock_account) get :show, :id => "37" end it "should render the found account as xml" do Account.should_receive(:find).and_return(mock_account) mock_account.should_receive(:to_xml).and_return("generated XML") get :show, :id => "1" response.body.should == "generated XML" endend
describe “responding to GET /accounts/new” do
it "should succeed" do get :new response.should be_success end it "should render the 'new' template" do get :new response.should render_template('new') end it "should create a new account" do Account.should_receive(:new) get :new end it "should assign the new account for the view" do Account.should_receive(:new).and_return(mock_account) get :new assigns[:account].should equal(mock_account) endend
describe “responding to GET /accounts/1/edit” do
it "should succeed" do Account.stub!(:find) get :edit, :id => "1" response.should be_success end it "should render the 'edit' template" do Account.stub!(:find) get :edit, :id => "1" response.should render_template('edit') end it "should find the requested account" do Account.should_receive(:find).with("37") get :edit, :id => "37" end it "should assign the found Account for the view" do Account.should_receive(:find).and_return(mock_account) get :edit, :id => "1" assigns[:account].should equal(mock_account) endend
describe “responding to POST /accounts” do
describe "with successful save" do it "should create a new account" do Account.should_receive(:new).with({'these' => 'params'}).and_return(mock_account) post :create, :account => {:these => 'params'} end it "should assign the created account for the view" do Account.stub!(:new).and_return(mock_account) post :create, :account => {} assigns(:account).should equal(mock_account) end it "should redirect to the created account" do Account.stub!(:new).and_return(mock_account) post :create, :account => {} response.should redirect_to(account_url(mock_account)) end end describe "with failed save" do it "should create a new account" do Account.should_receive(:new).with({'these' => 'params'}).and_return(mock_account(:save => false)) post :create, :account => {:these => 'params'} end it "should assign the invalid account for the view" do Account.stub!(:new).and_return(mock_account(:save => false)) post :create, :account => {} assigns(:account).should equal(mock_account) end it "should re-render the 'new' template" do Account.stub!(:new).and_return(mock_account(:save => false)) post :create, :account => {} response.should render_template('new') end endend
describe “responding to PUT /accounts/1″ do
describe "with successful update" do it "should find the requested account" do Account.should_receive(:find).with("37").and_return(mock_account) put :update, :id => "37" end it "should update the found account" do Account.stub!(:find).and_return(mock_account) mock_account.should_receive(:update_attributes).with({'these' => 'params'}) put :update, :id => "1", :account => {:these => 'params'} end it "should assign the found account for the view" do Account.stub!(:find).and_return(mock_account) put :update, :id => "1" assigns(:account).should equal(mock_account) end it "should redirect to the account" do Account.stub!(:find).and_return(mock_account) put :update, :id => "1" response.should redirect_to(account_url(mock_account)) end end describe "with failed update" do it "should find the requested account" do Account.should_receive(:find).with("37").and_return(mock_account(:update_attributes => false)) put :update, :id => "37" end it "should update the found account" do Account.stub!(:find).and_return(mock_account) mock_account.should_receive(:update_attributes).with({'these' => 'params'}) put :update, :id => "1", :account => {:these => 'params'} end it "should assign the found account for the view" do Account.stub!(:find).and_return(mock_account(:update_attributes => false)) put :update, :id => "1" assigns(:account).should equal(mock_account) end it "should re-render the 'edit' template" do Account.stub!(:find).and_return(mock_account(:update_attributes => false)) put :update, :id => "1" response.should render_template('edit') end endend
describe “responding to DELETE /accounts/1″ do
it "should find the account requested" do Account.should_receive(:find).with("37").and_return(mock_account) delete :destroy, :id => "37" end it "should call destroy on the found account" do Account.stub!(:find).and_return(mock_account) mock_account.should_receive(:destroy) delete :destroy, :id => "1" end it "should redirect to the accounts list" do Account.stub!(:find).and_return(mock_account) delete :destroy, :id => "1" response.should redirect_to(accounts_url) endend
end
script/spec spec/controllers/accounts_controller_spec.rb -fn
AccountsController
responding to GET /accounts
should succeed
should render the 'index' template
should find all accounts
should assign the found accounts for the view
responding to GET /accounts.xml
should succeed
should find all accounts
should render the found accounts as xml
responding to GET /accounts/1
should succeed
should render the 'show' template
should find the requested account
should assign the found account for the view
responding to GET /accounts/1.xml
should succeed
should find the account requested
should render the found account as xml
responding to GET /accounts/new
should succeed
should render the 'new' template
should create a new account
should assign the new account for the view
responding to GET /accounts/1/edit
should succeed
should render the 'edit' template
should find the requested account
should assign the found Account for the view
responding to POST /accounts
with successful save
should create a new account
should assign the created account for the view
should redirect to the created account
with failed save
should create a new account
should assign the invalid account for the view
should re-render the 'new' template
responding to PUT /accounts/1
with successful update
should find the requested account
should update the found account
should assign the found account for the view
should redirect to the account
with failed update
should find the requested account
should update the found account
should assign the found account for the view
should re-render the 'edit' template
responding to DELETE /accounts/1
should find the account requested
should call destroy on the found account
should redirect to the accounts list
Slides from RailsConf
June 16th, 2008
Here are the slides from my session at RailsConf on Integration Testing With RSpec’s Story Runner.
RSpec waving 'bye bye' to implicit module inclusion
May 29th, 2008
Until sometime very soon, when you describe a module in RSpec using this syntax:
describe SomeModule do ... end
RSpec implicitly includes that module in the example group. This allows you to do this:
describe CatLikeBehaviour do
it "should say 'meow' when it greets you" do
say_hello.should == 'meow'
end
end
module CatLikeBehaviour do
def say_hello
'meow'
end
end
As is often the case with things implicit, this actually turns out to be a problem. The problem revealed itself most notably when an RSpec user reported that a describe() method in a module he was using was conflicting with RSpec’s describe() method.
<p>One response to that thread suggested that using <code>describe()</code> in a module might be too generic, but I think that really hides the point. Imagine how frustrated you would get if you had examples of a module with a <code>current?</code> method and we decided to add a <code>current?</code> method to <code>Spec::Example::ExampleGroupMethods</code>. You’d suddenly start seeing those examples fail with stack traces eminating from RSpec instead of your code. Not good.</p>
<p>And so, we are going to be removing this feature.</p>
<p>With the 1.1.4 release, you get a warning any time that the example calls a method on <code>self</code> that is part of the included module. Soon it will be removed entirely.</p>
<h3>rails helper examples</h3>
<p>The biggest impact of this is going to be felt in rspec-rails helper examples. There are two remedies that you have if you’ve got examples that send messages to <code>self</code> that should be going to another object:</p>
<p>1. use the new <code>helper</code> object provided by <code>HelperExampleGroup</code></p>
describe DateHelper do
it "should format the date as mm/dd/yyyy" do
helper.format_date(Date.new(2008, 5, 31)).should == '05/31/2008'
end
end
The helper object is an instance of ActionView::Base with the named module included in it, so it has access to everything else that it should have.
<p>2. include the module explicitly</p>
describe DateHelper do
include DateHelper
it "should format the date as mm/dd/yyyy" do
format_date(Date.new(2008, 5, 31)).should == '05/31/2008'
end
end
My recommendation is definitely the first option as I find it more expressive.
<p>I realize this is <span class="caps">API</span> changing and backward-compatibility breaking, but this is one of those cases where, at least in my view, the pain is justified by the result.</p>
