I decided, at last, to investigate RSpec in the course of my work in Rails. I should be clear about something: I’m not writing Rails code for profit, only for fun and the experience, so I’m learning slowly and not really pushing the envelope the way I would on the job. Read this series with that in mind.
Here is the story I want to implement next: “Preview postings”. That story is for this very weblog. When I add an entry, I want to be able to preview it without publishing it. Of course, now that I write that, I realize that I might also want to preview edits to existing entries, but that’s another story. I suppose this story is “Preview new postings”. It’s good to know that I can still negotiate scope with myself!
Not being a big RSpec user yet, rather than get stuck writing a story test, I thought I’d do my bit of design, dive in, then RSpec-drive my model1.
First, I had to install RSpec and RSpec on Rails. That wasn’t too hard. I followed these instructions and issued these commands in the root of my Rails project:
ruby script/plugin install -x svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec
ruby script/plugin install -x svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec_on_rails
ruby script/generate rspec
After a few seconds, I could run my specs using the new Rake tasks in my project, like spec. When I run my specs, of course I have none, but I do see weird ANSI codes in my output. I’ve asked a question about that, and when I get an answer, I’ll let you know what I found out.
It’s time to write a spec. My design has a class Posting, so I figured I would add the notion of “publishing” a posting, so that I could save a posting without publishing it. This would allow me to preview it. For this, I decided to add a published_at timestamp. For that, I wrote this spec:
describe Posting do
it "should not yet be published" do
Posting.new.should_not be_published
end
end
What I find neat and disturbing at the same time is that RSpec magically looks for a method published? on Posting because I’ve used the “should… be” syntax. I think it’s neat, but what disturbs me is that I have to know where the underscores go. I expect to write that as Posting.new.should not_be_published or Posting.new.should_not_be published. Not exactly “least surprise” for me2.
I satisfy this spec by adding a published? method than answers false and move to the next spec.
describe WeblogController do
it "should be published when it has a published timestamp" do
new_posting = Posting.new(:published_at => DateTime.now)
new_posting.should be_published
end
end
To satisfy this spec, I add the column published_at to the postings table. When I migrate, though, I should mark all the current postings as published, so I make them published as of the time they were created. It’s not perfectly accurate, but it will do. I haven’t been tracking the last updated at timestamp. Here is the migration I wrote:
class EnablePreviewForPostings < ActiveRecord::Migration
def self.up
add_column :postings, :published_at, :datetime, :default => nil
Posting.find(:all).each { | each |
each.update_attribute(:published_at, each.created_at)
}
end
def self.down
remove_column :postings, :published_at
end
end
When I migrate the database, I see that all is well. Now I can implement published? like so:
class Posting < ActiveRecord::Base
def published?
!self.published_at.nil?
end
end
I’m green, so I can continue3. The essence of preview is that I can save the entry, display it, but not include it in the published entries. To that end, I need to retrospec the method that grabs all the postings so it’ll exclude unpublished ones. This is the first time I’m spec-ing a controller, so I’ll learn something. I create my new spec at $RAILS_ROOT/spec/controllers/posting_controller_spec.rb and write this:
describe WeblogController do
fixtures :postings
it "should only select published entries when browsing" do
get 'browse'
postings = assigns[:postings]
postings.size.should == 3 # 4 articles, 3 published
end
end
Unfortunately, this doesn’t find my existing postings fixture. I get this rude answer:
No such file or directory - /Users/jbrains/Workspaces/dauphin/jbrains.info/spec/fixtures/postings
(I should note that that’s not the exact output, but the exact output tripped some security filter at TextDrive, so I couldn’t paste it in. That only cost me a half hour to figure out. Thanks, TextDrive.)
So what does Google have to say about this? Curiously, one article says Lose the fixtures with rSpec, which I like, so I’m inclined to try it. I change my spec to this:
describe WeblogController do
it "should only select published entries when browsing" do
Posting.create!(
:published_at => nil,
:title => "Irrelevant detail", :content => "Irrelevant detail"
)
get 'browse'
postings = assigns[:postings]
Posting.find(:all).size.should 1
postings.size.should 0
end
end
I don’t like the irrelevant details, but when I try to switch to test doubles, it’s a dead end, so I stick with this spec and try to satisfy it. By adding the condition published_at is not null, I get the job done. The problem, though, is that there are two code paths in my browse method, depending on whether the URI is /browse or /browse/12. The latter means “show only articles in category 12”, which supports my entry tags. I wrote this before I knew about custom routes, so I could consider refactoring to a custom route and simplifying browse, but that’s more work than I’m willing to take on at 01.25, so I’ll leave a REFACTOR comment and add a spec for the /browse/12 case.
describe WeblogController do
it "should only select published entries when browsing within a single category" do
category = Category.create!(:name => "Irrelevant detail")
Posting.create!(
:published_at => nil,
:title => "Irrelevant detail", :content => "Irrelevant detail"
).categories << category
get 'browse', :id => category.id
postings = assigns[:postings]
Posting.find(:all).size.should 1
postings.size.should 0
end
end
That spec is violated, so I satisfy it. After some refactoring, browse looks like this:
class WeblogController < ApplicationController
def browse
if params[:id].nil?
@postings_pages, @postings = paginate :postings, :order_by => default_order, :conditions => [published_condition], :per_page => 5
else
# REFACTOR Move this to a custom route
@postings_pages, @postings = paginate_collection(:per_page => 5, :page => params[:page]) {
Posting.find_by_sql([
%Q(
select * from postings where postings.id =
any (select posting_id from tags where category_id = ?)
and (#{published_condition})
order by #{default_order}
), params[:id]
])
}
end
end
private
def published_condition
"published_at is not null"
end
end
I’m pretty happy with that, and I can’t think of another spec that might be violated, so I move on to the “preview” action. I imagine this is as simple as adding a “preview” button to the form where I add a posting which creates the entry without publishing it, then display it a new window. This way I can close the window, go back, then press “save” when I’m ready. I might even rename “save” to “publish” to emphasize the fact that I’d be publishing the entry. I need to do some more retrospec-ing (“retrospecking”?) to prepare for that.
describe PostingsController do
it "should not publish a new entry when I preview it" do
post 'preview'
new_posting = assigns[:posting]
new_posting.should_not be_published
end
end
I can satisfy that easily enough.
def preview
@posting = Posting.new(params[:posting])
@posting.save
redirect_to :action => "read", :id => @posting
end
Rather than pop up a window, I’ll just remember to use the “Back” button. My customer side wants a real-time preview, and it’s now 01.54, so since I’m not prepared to do that now, my programmer side will ship the simplest thing that could work, get feedback, then add the cool Web 2.0 crap. The next step is to add my preview button, but the scaffold code doesn’t directly handle two submits for the same form, so I need to do some research. The best solution I find is to add a name attribute to my buttons and “case” on the name in my controller method. Not pretty, but adequate. I refactored my new entry form to prepare for that, and when I did, I noticed I couldn’t see any new entries. Of course, that’s because create doesn’t publish. I was hoping to do that later, but I guess I need to do it now. Since I’m on a green bar, I can retrospec create.
describe PostingsController do
it "should publish a new entry when I publish it" do
post 'publish'
new_posting = assigns[:posting]
new_posting.should be_published
end
end
Evidently my subconscious insists I rename create to publish, so I do that. Now my spec is violated, so I satisfy it. In the process, I introduce the new method publish on Posting, which I’ve spec-driven.
class Posting < ActiveRecord::Base
def publish
self.published_at = DateTime.now
save
end
end
This makes the change to the controller easier.
def publish
@posting = Posting.new(params[:posting])
if @posting.publish
update_tags_for(@posting)
flash[:notice] = 'Posting was successfully created.'
redirect_to :controller => 'weblog'
else
render :action => 'new'
end
end
I only had to change @posting.save to @posting.publish. That looks like that works, so I try it manually through the UI4. Now I can publish a new entry, so I need to be able to preview one. It’s time for another spec.
describe PostingsController do
it "should preview when I press the 'Preview' button" do
post 'submit_new', :submit => "preview"
assigns[:posting].should_not be_published
end
end
Satisfying this spec is pretty straightforward, so I retrospec for the ‘Publish’ case. After a little refactoring, I have this:
class PostingsController < ApplicationController
def submit_new
self.send(params[:submit].downcase)
end
end
Clever, no? Perhaps too clever, but time will tell. It works for me. I’d worry about a security hole if I weren’t the only user who could submit this form.
A quick manual test reveals a problem: “Couldn’t find Posting without an ID.” It looks like I’m missing a preview-related spec, so I add one… and when I do, all hell breaks loose. Here’s the short version of the story:
- I noticed my
authorize filter wasn’t right, so I fixed it.
- I didn’t notice that this would cause all my specs to fail because they don’t login with an authorized user first.
- I didn’t notice that this was the problem, because I made a change to unspec-ed code and didn’t think anything of it.
Great how that happens, no? So I search for something on the topic of how to stub out filters, and I read this, which suggested I write
controller.stub!(:login_required).and_return(false)
to indicate that there’s no need to login. I tried that and it didn’t work, so I added this instead:
describe PostingsController do
before(:each) do
controller.stub!(:authorize)
end
end
Now everyone is authorized in my specs. With that distraction out of the way, I can get back to the point.
describe PostingsController do
it "shouldn't preview an incomplete entry" do
post 'preview', :title => "", :content => ""
assigns[:posting].should be_new_record
assert_redirected_to :action => "new"
end
end
This is violated because I redirect to the wrong place. It’s an easy fix, but with a little manual testing, I see that while “Publish” gives me nice error messages when it fails, “Preview” doesn’t. Upon further inspection, it’s because preview redirects to new on failure, rather than merely rendering it, as publish does. I should write a spec for that. This time, I add some expectations to the last spec I wrote.
describe PostingsController do
it "shouldn't preview an incomplete entry" do
post 'preview', :title => "", :content => ""
assigns[:posting].should be_new_record
response.should_not be_redirect
response.should render_template("new")
end
end
I notice that I had typed assert_redirected_to up there. Old habits die hard, especially when the fingers can work without the conscious mind. Frightening. This spec is violated because I redirect, so I fix that. Now I see that publish and preview are structurally similar.
def publish
@posting = Posting.new(params[:posting])
if @posting.publish
update_tags_for(@posting)
flash[:notice] = 'Posting was successfully created.'
redirect_to :controller => 'weblog'
else
render :action => 'new'
end
end
def preview
@posting = Posting.new(params[:posting])
if @posting.save
redirect_to :action => "read", :id => @posting.id
else
render :action => "new"
end
end
The differences? The method they invoke on @posting and the true branch of the if statement. It takes a little to refactor this, but I arrive at the following.
class PostingsController < ApplicationController
def publish
save_posting :publish, :on_success => lambda { | posting |
update_tags_for(posting)
flash[:notice] = 'Posting was successfully created.'
redirect_to :controller => 'weblog'
}
end
def preview
save_posting :preview, :on_success => lambda { | posting |
redirect_to :action => "read", :id => posting.id
}
end
private
def save_posting(save_action_symbol, events)
@posting = Posting.new(params[:posting])
if @posting.send(save_action_symbol)
events[:on_success].call(@posting)
else
render :action => 'new'
end
end
end
Now, I think, the feature works. Just to be sure, I try a few manual tests. I would prefer to rely on specs, but it’s 03.21 and I don’t plan on much more retrospec-ing tonight. One thing I notice is the “preview” view doesn’t say anything about being a preview, so I should fix that.
describe PostingsController do
it "should tell me a preview is a preview" do
post 'preview', :posting => {
:title => "Irrelevant detail",
:content => "Irrelevant detail"
}
response.should redirect_to(
:action => "preview",
:id => assigns[:posting].id
)
end
end
Making that pass is easy: I redirect to preview, rather than read. A little manual inspection reveals that nothing about the “preview” template makes it look like a preview, so I add a little color to make that more obvious. I’ll go with this for now:
<% @title = "Preview '#{@posting.title}'" %>
<div style="border: 1px solid; padding: 5px; background: pink">
<%= render :partial => "posting" %>
<p style="text-align: right">preview</p>
</div>
It looks reasonably good, and although I have to use the browser’s Back button to edit the draft, and although the “edit” button in the entry doesn’t do what I expect, I can live with it for now.
So, my first RSpec experience has been a good one. In spite of a couple of minor problems, I felt I proceeded steadily, if a little slowly. I imagine I could get used to this quite quickly. RSpec on Rails is a terrific plug-in.
Still, I’d prefer not to have to create real entries in order to write specs for my controllers. I’m sure there’s a better way, but learning what that is will have to wait for another day. It’s 03.43, I have to pack and get some sleep before traveling from Philadelphia to DC for Agile 2007. Before I go, though, I should deploy.
In the process of preparing to commit my changes to my code repository, I updated the RSpec plug-ins, and can no longer run my specs. It looks like a problem in one of the plug-ins. Needless to say I’m not impressed with that. Fortunately, it’s not a big problem: I simply needed to do script/generate rspec again, and all was well. As a result, I’d better freeze my plug-ins before deploying. How do I do that?
Ask, and ye shall receive.
So while that took longer than it usually does, I was able to preview this entry with the new feature, so I’m satisfied with it. The next step might be to add real-time preview or the primitive preview feature when editing an entry. Either way, I learned a lot, and I think I’ll be more comfortable spec-ing my next new feature.
1 What’s the verb for doing BDD? In TDD, we say “test-drive”. What do I say when doing BDD with RSpec? Write me and let me know.
2 I tried Posting.new.should_not_be published, but it didn’t work. Shame.
3 You can change some of the language of TDD, but you’ll take my green bar from my cold… well, you get the idea.
4 Yes; I wrote untested, unspec-ed code. I’m not proud of it. Why do you think I’m writing this article?
Discuss