Err the Blog Atom Feed Icon
Err the Blog
Rubyisms and Railities
  • “Be Dee Dee and Me”
    – Chris on May 02, 2007

    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!

  • Erik Kastner, 2 days later:

    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?

  • Kevin Marsh, 2 days later:

    Another great post from a prolific Rails blogger! Keep it up defunkt. You da man.

  • Dave O., 2 days later:

    Great post.

  • Curtis, 2 days later:

    I didn’t even know test/spec existed, rock on. Thanks.

  • Ruby on Rails, 2 days later:

    Another Great post from errtheblog. Excellent writeup on BDD. Your test concepts are quite clean.

  • Daniel Fischer, 2 days later:

    Exactly what I was needing to read. Thank you very much for this excellent post!

  • James H, 2 days later:

    F’n great post. Thank you very much.

  • atmos, 4 days later:

    You guys fuckin’ suck. Top notch as always. :)

  • mike, 6 days later:

    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!

  • James H, 18 days later:

    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?

  • Aaron Wheeler, about 1 month later:

    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.

  • David Chelimsky, about 1 month later:

    “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.

  • Matt, 3 months later:

    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.

  • Thirteen people have commented.
    Chime in.
    Sorry, no more comments :(
This is Err, the weblog of PJ Hyett and Chris Wanstrath.
All original content copyright ©2006-2008 the aforementioned.