Since before January (I don’t know how far before), I’ve been writing all my tests using the Cool New Way. Not because it’s cool, or new, but because it really helps me write better code. Let me tell you about it.
A Brief and Subtle Introduction
Be dee dee. Behavior driven development. BDD is a way of writing code by testing what the code should do rather than testing the code itself. Does that make sense?
Here’s an example:
require 'test/spec' context "This blog post" do setup do @post = BlogPost.find_by_title('Be Dee Dee') end specify "should mention bdd" do @post.body.should.include 'behavior driven development' end specify "should be concise" do @post.body.word_count.should.be < 1000 end specify "should contain at least four hyperlinks" do @post.body.scan('a href').size.should.be >= 4 end specify "should be written by me" do @post.author.should.equal Author.find_by_name('Chris Wanstrath') end end
So, BDD is just writing tests with a special syntax and describing them in plain jane English. “What’s the big deal, then?” Nothing. There is no big deal.
However, if you’ve a bit of time and an inclination for writing awesomely readable tests (which may ultimately help you write better code), continue.
Say, for instance, the above was real code. I could run it with a special test runner and get this output:
This blog post: - should mention bdd - should be concise - should contain at least four hyperlinks - should be written by me Finished in 0.0083 seconds. 4 specifications (4 requirements), 0 failures
Nice. Let’s see where this goes.
Choose Your Weapon
A super popular BBD framework is RSpec, which I’m sure you’ve heard of. Those guys are leading the Ruby BDD revolution, more or less, with their code and blog posts.
RSpec is catching on with some big time projects adopting it, among them Rubinius and caboose(read: court3nay)’s sample app. Originally mofo used RSpec, but I’ve since moved on.
To test/spec, by Christian Neukirchen (of Anarchaia fame). His announcement of test/spec 0.3 is a very thorough explanation of the library’s features. This is the library I’ll be focusing on hereafter.
The test/spec library is great because it simply wraps test/unit, meaning most of your test/unit tools work right out of the box with it. No need to do anything special to test Rails (rake test, et al). ZenTest’s autotest “just works.” Heckle and it get along amazingly well. You can use plain ol’ assert_equal in specs. Etc.
As a result, you can start slipping test/spec into your existing project, alongside your existing tests, without doing anything special. A spec here, a spec there, and before you know it the whole damn crew is BDDin’.
But, don’t let me boss you around. A compulsory search on Rubyforge reveals two more BDD libraries: SpecUnit and RBehave, the latter being developed by the dude who coined the ‘BDD’ term: Dan North. Worth a look or, at least, a gander.
Digging a Rails Model BDD-Style
For now we’re going with test/spec: $ sudo gem install test-spec
(Yes, a dash. But you require it with a slash. You’ll get used to it.)
Luke Redpath posted an article on BDDing a Rails model using RSpec back in August. It’s good and thorough, so I’m not going to re-say what he said. Read it.
Anyway, let’s write a model. Digg got all crazy the other day with this HD-DVD bidness, so we’re gonna start our own Digg to make sure we can consistently get news about the stuff we’re interested in: Obama, the Wii, and Apple rumors.
(If you’d like to follow along at home, the completed source can be found at require. Check it out with svn and set everything up with $ rake db:bootstrap.)
$ svn co svn://errtheblog.com/svn/projects/dugg
So! The first thing we’d do here is describe our model’s ideal behave. In English. With YAML.
A User (in general): - should be able to digg a story - should not be able to digg a story twice - should be able to tell if he has dugg a story
Simple enough. Save this as user_spec.yml and then run the yaml_to_spec rake task (this one) on it to generate a scaffold:
$ rake -s yaml_to_spec FILE=user_spec.yml context "A User (in general)" do xspecify "should be able to digg a story" do end xspecify "should not be able to digg a story twice" do end xspecify "should be able to tell if he has dugg a story" do end end
Put that into test/unit/user_test.rb and you’re mostly ready to roll. What’s xspecify? It means a spec is not yet ready to be tested—when you run this file, all the xspecify blocks will be ignored. Helpful when writing spec/tests before writing any code (which is exactly what we’re doing).
Anyway, let’s fill in those specs:
require File.dirname(__FILE__) + '/../test_helper' context "A User (in general)" do setup do @user = users(:defunkt) @story = stories(:undugg) @dugg_story = @user.dugg_stories.first end specify "should be able to digg a story" do @user.dugg_stories.should.not.include @story @user.digg(@story).should.equal true @user.dugg_stories(true).should.include @story end specify "should not be able to digg a story twice" do @user.digg(@dugg_story).should.equal false end specify "should be able to tell if he has dugg a story" do @user.should.have.dugg @dugg_story @user.should.not.have.dugg @story end end
This describes, to an extent, the behavior we want: digg should return true or false depending on whether or not it succeeds.
Running this test at this point gives a whole grip o’ errors. Time to start writing code.
Here’s our User, with the digg and dugg? methods simply wrapping our association:
class User < ActiveRecord::Base has_many :dugg has_many :dugg_stories, :through => :dugg, :source => :story def digg(story) dugg_stories << story rescue ActiveRecord::RecordInvalid false else true end def dugg?(story) dugg_stories.include? story end end
Then, of course, our Dugg model:
class Dugg < ActiveRecord::Base set_table_name 'dugg' belongs_to :user belongs_to :story validates_uniqueness_of :story_id, :scope => :user_id, :message => "can't be dugg again!" end
The Story class has nothing in it (yet).
Running the tests now gives us the green. Fantastics.
Finding Dugg’d Stories
Let’s add another feature, something to the Story class. How about finding the most dugg stories?
Here, a spec for that:
A Story (in general): - should return its title when sent #to_s Trying to find the most dugg stories: - should return a list of popular stories - should respect an arbitrary limit
Now, a test/spec for that:
require File.dirname(__FILE__) + '/../test_helper' context "A Story (in general)" do setup do @story = Story.find(:first) end specify "should return its title when sent #to_s" do @story.to_s.should.equal @story.title end end context "Trying to find the most dugg stories" do setup do @second_most_popular, @most_popular = Story.find(:all).last(3) end specify "should return a list of popular stories" do popular = Story.most_dugg popular.size.should.equal 5 popular.first.should.equal @most_popular end specify "should respect an arbitrary limit" do popular = Story.most_dugg(2) popular.size.should.equal 2 popular.shift.should.equal @most_popular popular.shift.should.equal @second_most_popular end end
I setup my fixtures to make the last story insert’d not dugg at all, second last story insert’d the most popular, the third last story insert’d the second most popular, etc. Keep that in mind; it’s clutch.
This spec fails, of course. We don’t have a most_dugg method and our to_s method is the default. Let’s fix that.
class Story < ActiveRecord::Base alias_attribute :to_s, :title def self.most_dugg(limit = 5) connection.select_all(<<-SQL).map { |row| find(row['story_id']) } SELECT story_id, count(1) as size FROM dugg GROUP BY story_id ORDER BY size DESC LIMIT #{limit} SQL end end
Lil’ bit of SQL never hurt no one. Not the most efficient query, but we just want to get our specs passing—we can focus on optimization later (with a counter_cache or something).
And pass they do. Glory be, be dee dee.
Gettin’ Railsy: BDDin’ Controllers
What’s cool is we haven’t even started Mongrel yet. Web browsers are so last year. Let’s keep it up by adding a controller or two.
Hold on. There’s a catch: functional tests in Rails are more than test cases with stock test/unit assertions. There’s assert_redirected_to and assert_select and all that webby goodness. Does test/spec give us this stuff? No, it doesn’t.
However, test/spec/rails does. A plugin by Per Wigren, the README file explains it all quite well. So does the cheat sheet.
Install it:
$ cd vendor/plugins $ piston import http://svn.techno-weenie.net/projects/plugins/test_spec_on_rails
Now add require ‘test/spec/rails’ to your test_helper.rb and you’re good to go.
Stories Stories Stories
Time to hussle. We want to list all the stories on a page, in order. Nothing fancy. We also want to show the user whether or not she’s already dugg the story.
Some people like to do their controller specs a bit differently, so take this all with the same grain of salt you take everything on the internet.
A page listing all stories: - should display every story - should show a 'digg' link if the viewing user has not dugg the story - should not show a 'digg' link if the viewing user has dugg the story
We’re going to sprinkle a bit of Mocha into this spec to pretend we’re always logged in as defunkt. Add require ‘mocha’ to your test_helper.rb after doing a $ sudo gem install mocha. (If you don’t know what the hell I’m talking about, check this chocolately post from the past.)
Speccin’ it out:
require File.dirname(__FILE__) + '/../test_helper' context "A page listing all stories" do use_controller StoriesController setup do @user = users(:defunkt) @dugg = stories(:digg) @not_dugg = stories(:undugg) @controller.stubs(:current_user).returns(@user) get :index end specify "should display every story" do status.should.be :success template.should.be 'stories/index' assigns(:stories).size.should.equal Story.count body.scan(/story_/).size.should.equal Story.count end specify "should show a 'digg' link if the viewing user has not dugg the story" do dom_id = "#story_#{@not_dugg.id}" should.select "#{dom_id}>.digg" end specify "should not show a 'digg' link if the viewing user has dugg the story" do dom_id = "#story_#{@dugg.id}" should.not.select "#{dom_id}>.digg" end end
Reads nicely, huh? And not too distant from a test/unit functional test.
The use_controller method will setup all the instance variables we normally set ourselves: the request, response, and controller objects.
In our first spec we make sure the response is a-okay, then make sure we have the same number of ‘story_x’ ids as we do stories. Pagination is for sissies.
In the next two tests we ensure the ‘digg’ link works as planned.
The cheat sheet, again, has all the info you could want about the stuff test/spec/rails gives you. It’s pretty much ripped straight from the README.
With all that done, it’s simply a matter of making the tests pass. And we do, of course.
The first thing to setup are our RESTful routes:
ActionController::Routing::Routes.draw do |map| map.resources :stories do |story| story.resources :diggs end map.home '', :controller => 'stories' end
Next we create our controller:
class StoriesController < ApplicationController def index @stories = Story.find(:all, :order => 'created_at DESC') end end
Then, if you will, the view:
<h1>All Stories!</h1> <% @stories.each do |story| %> <div id="<%= dom_id story %>"><%= digg_link story %><%= story %></div> <% end %>
Finally, the helpers:
module ApplicationHelper def digg_link(story) return if current_user.dugg? story link_to 'Digg!', story_diggs_url(story), :class => 'digg', :method => :post end def dom_id(record) "#{record.class.name.underscore}_#{record.id}" end end
See that digg_link method? The link itself doesn’t actually work yet, but we can do it. I know we can.
Diggin’ It
Okay, the DiggsController. What we want to assert first is our ability to digg a story. So, to create a digg:
Successfully digging a story: - should redirect to the stories listing page
Which brings us to:
require File.dirname(__FILE__) + '/../test_helper' context "Successfully digging a story" do use_controller DiggsController setup do @user = users(:defunkt) @story = stories(:undugg) @controller.expects(:current_user).returns(@user) @user.expects(:digg).with(@story).returns(true) post :create, { :story_id => @story.id } end specify "should redirect to the stories listing page" do flash[:notice].should.equal 'Dugg!' should.redirect_to stories_url end end
Simple and beautiful, one hopes.
The first error is test/spec bemoaning the lack of a DiggsController constant, so go ahead and add that. Make an empty create method while you’re in there, too. Remember, we’re just trying to make stuff pass at this point. We’ll cross every bridge as we come to it.
Now it’s complaining about the create.html.erb template not existing—time to write our create method:
class DiggsController < ApplicationController def create story = Story.find(params[:story_id]) if current_user.digg(story) flash[:notice] = 'Dugg!' redirect_to stories_path end end end
Disco. But, remember, digg can fail. Let’s handle that case with another context:
context "A failed story digging attempt" do use_controller DiggsController setup do @user = users(:defunkt) @story = stories(:dugg) @controller.expects(:current_user).returns(@user) @user.expects(:digg).with(@story).returns(false) post :create, { :story_id => @story.id } end specify "should render with an error message" do flash[:notice].should.match /error/i should.redirect_to stories_url end end
Here, the new controller code:
class DiggsController < ApplicationController def create story = Story.find(params[:story_id]) if current_user.digg(story) flash[:notice] = 'Dugg!' else flash[:notice] = 'Error digging :(' end redirect_to stories_path end end
And just like that, we’re passing. Apple rumors, here we come.
The Full Spec
Oh, for bonus, remember that “special test runner” I told you about? You are now fully qualified to use it:
$ ruby test/functional/diggs_controller_test.rb -r s Successfully digging a story - should redirect to the stories listing page A failed story digging attempt - should render with an error message Finished in 0.101304 seconds. 2 specifications (8 requirements), 0 failures
Rad. But there’s more, given you download these double bonus test/spec/rails tasks:
$ rake spec A Story (in general) - should give its title when asked for a string representation of itself Trying to find the most dugg stories - should return a list of popular stories - should respect an arbitrary limit A User (in general) - should be able to digg a story - should not be able to digg a story twice - should be able to tell if he has dugg a story Finished in 0.136295 seconds. 6 specifications (10 requirements), 0 failures Successfully digging a story - should redirect to the stories listing page A failed story digging attempt - should render with an error message A page listing all stories - should display every story - should show a 'digg' link if the viewing user has not dugg the story - should not show a 'digg' link if the viewing user has dugg the story Finished in 0.186009 seconds. 5 specifications (9 requirements), 0 failures
That’s it, that’s everything our app does. Right there.
Testing is Fun Again!
If you want to feel around some more examples check out the cache_fu specs or the Gibberish specs or even the mofo specs. Mephisto, I hear, has some test/spec action these days.
Wanting to test/spec integration tests in Rails? Brian has the solution, which he busted out at a SF Ruby Meetup hackfest.
Let me know if I missed anything, if I broke anything, or if you know of any public projects using test/spec. The best way to get into this stuff, as always, is to just do it. Go go go!
This is great stuff. I’ve been dying for a test/spec err post! Working on any cool projects that are digg-like these days?
Another great post from a prolific Rails blogger! Keep it up defunkt. You da man.
Great post.
I didn’t even know test/spec existed, rock on. Thanks.
Another Great post from errtheblog. Excellent writeup on BDD. Your test concepts are quite clean.
Exactly what I was needing to read. Thank you very much for this excellent post!
F’n great post. Thank you very much.
You guys fuckin’ suck. Top notch as always. :)
Thanks for the post. I’ve already written and implemented my first spec thanks to you! Thanks as well for posting the source code.
I did find one problem for any of those folks like me that started from scratch with test/spec after reading your post. In your specs near the top, you use “should.have” and “should.not.have” without noting that “have” is just an alias for “be”. I found that out by poking through your test_helper.rb in the dugg project.
Thanks again!
When using the usual test/unit framework for functional tests we can define helper methods within the class for doing specialized tasks. For example, in one of my functional tests I have a method for helping to upload photographs. How do you handle these sorts of specialized methods in test/spec?
This might be obvious, but the strings that come after the ‘context’ method call should be unique. If they are not, then the calls to ‘setup’ will be compounded, which can cause issues with instance variables with similar names.
This is especially problematic with functional tests, where the @controller will be corrupted.
“The test/spec library is great because it simply wraps test/unit, meaning most of your test/unit tools work right out of the box with it. No need to do anything special to test Rails (rake test, et al). ZenTest’s autotest “just works.” Heckle and it get along amazingly well. You can use plain ol’ assert_equal in specs. Etc.”
Actually, this is true of spec/rails (RSpec’s Rails plugin). There is rcov, heckle and autotest integration and spec/rails wraps test/unit so you get all of the test/unit assertions as well.
If you prefer test/spec’s style, that’s one thing, but I hate to see people making a choice based on inaccurate information.
Has anyone found a way around the:
Unable to map class New signup for mailing list to a file
...when using autotest with test/spec? Particularly with big test files, this can be a dealbreaker, for me. This is pretty much the last hurdle I have to total adoption.
Chime in.