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>
RSpec-1.1.4
May 27th, 2008
We released RSpec-1.1.4 today. It’s mostly a maintenance release but there are a few of cool new features that you may want to know about and take advantage of.
<h3>hash_including</h3>
<p>One thing that has always been a drag is having to specify every key/value pair in a hash that is received as an argument. This is especially painful in Rails controller examples because Rails adds some data to the hash and the examples really don’t care about that extra data.</p>
<p>Enter <code>hash_including()</code>.</p>
<p>This is a mock argument matcher that let’s you expect a hash including certain key/value pairs regardless of anything else that shows up in the hash. So instead of:</p>
account.should_receive(:deposit).with({:amount => 37.42, :date => anything()})
you can just say:
account.should_receive(:deposit).with(hash_including(:amount => 37.42))
and keep the example focused on what you’re really interested in
<p>Thanks to <a href="http://talklikeaduck.denhaven2.com/">Rick DeNatale</a> who submitted this feature request and the patch to implement it.</p>
<h3>The heckler returns</h3>
<p>RSpec wasn’t correctly supporting heckle for a while but the spec-heckler is back in action. For those unfamiliar, you can read about heckle at <a href="http://blog.zenspider.com/2007/06/heckle-version-141-has-been-re.html">zenspider’s blog</a>.</p>
<p>Here’s how you heckle your Animal model in your PetStore app:</p>
spec spec/models/animal_spec.rb --heckle Animal
Thanks to Antti Tarvainen for resurrecting this one.
<h3>stub_model</h3>
<p>This is for rails developers who like writing view examples with <code>mock_model()</code> but are sick and tired of having to stub every single attribute that gets referenced in a view.</p>
<p>Instead of creating a mock object like <code>mock_model()</code> does, <code>stub_model()</code> creates an instance of a real model class, but cuts off it’s connection to the database, raising an error any time it tries to connect to the database.</p>
<p>This is inspired by projects like <a href="http://www.dcmanges.com/blog/rails-unit-record-test-without-the-database">unit_record</a> and <a href="http://agilewebdevelopment.com/plugins/nulldb">NullDB</a>, but let’s you do things at a more granular level – allowing you to hit the db in some cases (where you think you really need it) and not in others.</p>
<p>Of course, you may prefer to the sort of “protection” you get from those projects, which ensure that no code touches the DB at all. If you do, have at it. This is just another option for you.</p>
<h3>All this and more</h3>
<p>These are just a few of the issues addressed in 1.1.4. For more information, check out the <a href="http://rspec.info/changes.html">changelog</a> and <a href="http://rspec.lighthouseapp.com/projects/5645">lighthouse</a>.</p>
The "BDD Story Template"
April 28th, 2008
Mike Cohn just wrote a nice post justifying what has become the preferred template for stories in BDD:
<p>As a <i>role</i><br />
I want feature
So that benefit
RSpec at github
April 9th, 2008
After a few months of exploring git and hosting RSpec’s git repository at github, we’re happy to announce that github is now RSpec’s official home for Source Code Management.
<p>Tracking will continue to live at the <a href="http://rspec.lighthouseapp.com">lighthouse</a>.</p>
<p>We will continue to release gems to <a href="Rubyforge">http://rubyforge.org/projects/rspec</a>, but we will no longer be committing changes to the subversion repository there. For Rails users who are using the rspec plugins for Rails, edge rails now supports git-hosted plugins.</p>
<p>We’ve broken the project up into four separate repositories:</p>
<ul>
<li><a href="http://github.com/dchelimsky/rspec/wikis/home">rspec</a> for the rspec gem/plugin</li>
<li><a href="http://github.com/dchelimsky/rspec-rails/wikis/home">rspec-rails</a> for the rspec-rails gem/plugin (formerly rspec_on_rails)</li>
<li><a href="http://github.com/dchelimsky/rspec-tmbundle/wikis/home">rspec-tmbundle</a> for the TextMate bundle</li>
<li><a href="http://github.com/dchelimsky/rspec-dev/wikis/home">rspec-dev</a> for developers/contributors</li>
</ul>
<p>See the wikis for each repository for more information about building, installing and contributing to the project.</p>
Welcome Pat Maddox
April 4th, 2008
I’m pleased to announce that Pat Maddox is joining the RSpec Development Team.
<p>As you may already know, Pat has been contributing great patches and actively participating on the <a href="http://rubyforge.org/mailman/listinfo/rspec-users">rspec-users</a> and <a href="http://rubyforge.org/mailman/listinfo/rspec-devel">rspec-devel</a> mailing lists for quite some time. He has demonstrated a deep understanding of Behaviour Driven Development in general, and specifically as it applies to Rails, which has certainly posed some of the more interesting questions on our mailing lists.</p>
<p>We are all excited to have Pat on board and look forward to his continued contribution.</p>
ETEC Slides
April 1st, 2008
Here are the slides from my presentation at Emerging Technologies for the Enterprise on Integration Testing with RSpec’s Story Runner.
Presenting at ETEC
February 4th, 2008
I’m going to be presenting at ETEC on RSpec’s Story Runner. This will be more or less the same talk I’m presenting at RailsConf in May, except that it’s in March and on the other coast.
<p>See you in Philly!</p>
RSpec-1.1.3 and ZenTest-3.9.1
February 4th, 2008
ZenTest’s last two releases are not compatible with previous versions of RSpec. This is good news because Autotest now exposes better extension points for subclasses like those that ship with RSpec. Before, RSpec had to monkey patch Autotest to control the mappings of specs to files to run, and the list of files/directories to ignore. Now RSpec gets to use public methods (instead of instance variables) and documented hooks to do it’s work.
<p>In the long run, this will keep things more flexible for both RSpec and ZenTest. In the short run, the catch for you is that you have to use compatible versions of RSpec and ZenTest. They are:</p>
| RSpec version | ZenTest version |
|---|---|
| <= 1.1.1 | <= 3.7.x |
| 1.1.2 | 3.8.x |
| 1.1.3 | 3.9.x |

