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]
end

end

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"
end

end

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)
end

end

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"
end

end

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)
end

end

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)
end

end

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

end

end

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

end

end

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)
end

end

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.

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&#8217;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&#8217;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&#8217;t care about that extra data.</p>


<p>Enter <code>hash_including()</code>.</p>


<p>This is a mock argument matcher that let&#8217;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&#8217;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&#8217;s blog</a>.</p>


<p>Here&#8217;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&#8217;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&#8217;s you do things at a more granular level &#8211; 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 &#8220;protection&#8221; 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&#8217;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 versionZenTest version
<= 1.1.1<= 3.7.x
1.1.23.8.x
1.1.33.9.x