Ruby on Rails
HowToDoTestDrivenDevelopmentInRails

Test-driven development is A Good Thing. There are many sites out there that will give you abstract discussions on why you should be doing it, so this page won’t dwell too much on that. This is intended to give a basic ‘recipe’ to follow, mostly for those new to the subject. You should read HowtoFunctionalTest and HowtoUnitTest , if you haven’t already.

Note that none of this is my own originality at work – It’s been pieced together from the wise words of people like Dave Thomas and David Heinemeier Hansson of Agile Web Development, also Ben Griffiths
So, if this is useful – thank them. If it’s not, blame me!

In a nutshell, test-driven development means writing tests before you start the implementation of a piece of functionality. From the (excellent) book, Agile Web Development with Rails – “Think of it as a specification for how you want the code to work. When the test passes, you know you’re done coding. Better yet, you’ve added one more test to the application.”

Here is how I would go about this process in Rails. I’ll be taking the example of a simple user authentication system controller. At this point, you haven’t thought of any of the specifics, just a vague idea of what you want.

Generate your new controller

Do this in the normal way, lets say we make a user_controller, and what do you know! there are tests already set up. That’s a good start.

Open up the functional test

It’s in ‘test/functional/user_controller_test.rb’. Now is when you’re going to start planning your implementation. How will you know that your login system is working?

Let’s think.

The controller should store the user in the session on a successful login, right?

def test_should_store_user_in_session_on_successful_login

end

That was easy! Okay, well, it should not store the user in the session on an unsuccessful login.

def test_should_not_store_user_in_session_on_unsuccessful_login

end

This is going well! Other criteria could be things like “should redirect to page stored in session on successful login”, or “should reject signup when passwords do not match”. Keep adding these stubs to your test class as you work out how your controller should act.

Fill in the blanks.

Now you’ve got a list of test stubs, and you need to fill in the details. An example:

def test_should_redirect_to_page_in_session_on_successful_login
    @request.session[:return_to] =  "/bogus/location"
    login_with_valid_user
    assert_redirected_to "/bogus/location"
  end

Obviously, it’s going to fail – we don’t have anything written yet! But that will change.

DRY!

Well, now we’ve written a bunch of nice tests, and we now have a really solid idea of how it should be implemented. Go to here . really. That’s Ben Griffith’s blog, and he’s to thank for this next piece of magic.

In the root of your rails app, there’s a file called “Rakefile”. Open it up and right at the bottom, paste this:

task :agiledox do
  tests = FileList['test/**/*_test.rb']
  tests.each do |file|
    m = %r".*/([^/].*)_test.rb".match(file)
    puts m[1]+" should:\n"
    test_definitions = File::readlines(file).select {|line| line =~ /.*def test.*/}
    test_definitions.each do |definition|
      m = %r"test_(should_)?(.*)".match(definition)
      puts " - "+m[2].gsub(/_/," ")
    end
    puts "\n"
  end
end

Or, put this code in a file called agiledox.rake in your lib/tasks/ directory. Create the directory if its not there.

Now, navigate to your rails application in a command prompt, and run the command rake agiledox

You’ll be greeted with a nicely formatted list of everything your controller should do:

E:\Work\AZCTrunk>
user_controller should:
 - store user in session on successful login
 - not store user in session on unsuccessful login
 - redirect to page in session on successful login
 - redirect on successful login even if no page stored in session
 - store user in session on successful signup
 - not store user in session on unsuccessful signup
 - redirect to page in session on successful signup
 - reject signup when passwords do not match
 - reject signup when username is too short
 - reject signup with multiple errors
 - destroy user in session on logout

And that’s taken straight from your tests!. That’s why we gave our tests those long, descriptive names… “test_should_….on_….”.

The Easy Bit.

Now you can copy and paste that nice list of criteria straight as a comment into your blank user_controller.rb. You can now use that list as prescriptive guide to writing your methods. Now you won’t miss obvious things, or spend time thinking about how things should work. Just code. How do you know when you’re done? Well, your tests pass – of course.

You’ve written some tests, and gotten a list of specifications, and a start on your documentation… for free.

Thoughts.

I find this method works wonders, and I’ve started using it consistently. Remember, though, that in rails it’s okay to change your mind. You can’t be expected to think of everything when you first sit down and write your blank “should do…” list. That’s okay – if you think of something new, just go back to your tests, fill it in, “rake agiledox”, and you’re back to coding again.

Comments.

“To everyone out there who actually knows what they’re doing, please feel free to change this if you think you can improve on it, I’m really not an expert – I just wanted to try giving something useful to the Rails community to whom I owe so much — Tobin Jones”

Ulyses posted ActiveRecord unittest test helper code to simplify testing AR code.
<br \>

Sweet! One more thing: the above agiledox rake task blows up, because i2 is inserting some wiki syntax in the middle. Please go to Ben Griffith’s blog mentioned above and get the correct code.

Here’s the direct link: http://www.reevoo.com/blogs/bengriffiths/2005/06/24/a-test-by-any-other-name/

I tend to stub out lots of tests early on as reminders, but because the blank tests all succeed, it’s easy to forget them – rake test produces a nice list of passes and it seems a shame to add any code to spoil it… ;) I stick the following in test/test_helper.rb:

def no_test
  flunk "Test hasn't been written yet."
end

and then all the stub tests look like:

def test_should_do_something
  no_test
end

def test_should_do_something_else
  no_test
end

That way all the unwritten tests remain visible…

—-

I’ve changed the agiledox.rake file, so it writes the tests directly into a comment block into the right models and controllers. No more copying and pasting!!

desc "Generate agiledox-like documentation for tests"
task :agiledox do
  tests = FileList['test/**/*_test.rb']
  tests.each do |file|
    dname = File::dirname(file)
    if dname == 'test/unit'
      dname = 'app/models/'
    elsif dname == 'test/functional'
      dname = 'app/controllers/'
    else
      next
    end
    fname = File::basename(file, '_test.rb') + '.rb'
    unless File.exist?(dname + fname) then next end
    puts "processing " + file + "\n"
    test_definitions = File::readlines(file).select {|line| line =~ /.*def test.*/}
    filestr = File.read(dname + fname)
    filecont = Regexp.new("(# RAKE AGILEDOX RESULTS BEGIN.*# RAKE AGILEDOX RESULTS END[\n])?(.*)", Regexp::MULTILINE).match(filestr)[2]
    filestr = "# RAKE AGILEDOX RESULTS BEGIN\n"
    test_definitions.each do |definition|
      m = %r"test_(should_)?(.*)".match(definition)
      filestr += "# - " + m[2].gsub(/_/," ") + "\n"
    end
    filestr += "# RAKE AGILEDOX RESULTS END\n"
    File.open(dname + fname, "w") { |file|
      file << filestr << filecont
    }
  end
end

—-

I’ve recently started to use the Test::Rails components from ZenTest in my Rails projects and have modified the “agiledox” task above to provide slightly more readable (IMO) output:


namespace :test do
  namespace :doc do
    def indefinite_article(word)
      (word.to_s.downcase =~ /^[aeoi]/) ? 'An' : 'A'
    end

    def print_class_name(class_name)
      class_name = class_name.to_s.gsub(/([A-Z])/, ' \1').strip
      puts "\n#{indefinite_article(class_name)} #{class_name}:"
    end

    task :units do
      tests = FileList['test/unit/*_test.rb']
      tests.each do |file|
        File.foreach(file) do |line|
          case line
            when /^\s*class ([A-Za-z]+)Test/
              print_class_name($1)
            when /^\s*def test_([A-Za-z_]+)/
              puts "  - #{$1.gsub(/_/, ' ')}"
          end
        end
      end
    end

    task :functionals do
      tests = FileList['test/functional/*_test.rb']
      classes = {}
      current_class = nil
      print_actions = lambda do |actions|
        actions.each do |action, tests|
          puts "  '#{action}' action:"
          tests.each {|test| puts "    - #{test}"}
        end
      end

      tests.each do |file|
        File.foreach(file) do |line|
          case line
            when /^\s*class ([A-Za-z]+)Test/
              classes[$1] = {}
              current_class = $1
            when /^\s*def test_([A-Za-z]+)_([A-Za-z_]+)/
              classes[current_class][$1] ||= []
              classes[current_class][$1] << $2.gsub(/_/, ' ')
          end
        end
      end
      classes.each do |class_name, actions|
        next if actions.nil? || actions.empty?

        print_class_name(class_name + "'s")
        print_actions.call actions
      end
    end
  end

  task :doc => ['doc:units', 'doc:functionals']
end

This provides three new Rake tasks: test:doc:units, test:doc:controllers, and test:doc which runs both :units and :controllers.

The controller tests are assummed to take the form described in the Test::Rails::TestControllerTestCase documentation from ZenTest. This basically means that for things to work properly your controller tests should take the form:

test_[action]_[test description]

—-

I’ve added processing of RSpec on Rails spec files as well as the skipping of test/spec definitions that are commented out (detected by requiring that only whitespace is allowed on the line before the first word of the definition [viz., “def” or "it"]):


desc 'Generate agiledox-like documentation for tests and specs'
task :agiledox do
  process_test_or_spec_files( 'test' )
  process_test_or_spec_files( 'spec' )
end

def process_test_or_spec_files( file_type )
  case file_type
    when 'test'
      definition_line_regexp = /^\s*def\s+test.*/
      definition_substring_regexp = %r"test_(should_)?(.*)"
      definition_substring_index = 2
    when 'spec'
      definition_line_regexp = /^\s*it\s+(['"]).*\1\s+do.*/
      definition_substring_regexp = /it\s+(['"])(should )?(.*?)\1\s+do/
      definition_substring_index = 3
  end

  files = FileList["#{file_type}/**/*_#{file_type}.rb"]
  files.each do |file|
    dname = File::dirname(file)

    case file_type
      when 'test'
        case dname
          when "#{file_type}/unit"
            dname = 'app/models/'
          when "#{file_type}/functional"
            dname = 'app/controllers/'
          else
            next
        end
      when 'spec'
        case dname
          when "#{file_type}/models"
            dname = 'app/models/'
          when "#{file_type}/controllers"
            dname = 'app/controllers/'
          when "#{file_type}/views"
            dname = 'app/views/'
          else
            next
        end
    end
    fname = File::basename(file, "_#{file_type}.rb") + '.rb'
    unless File.exist?(dname + fname) then next end
    puts "processing " + file + "\n"
    definitions = File::readlines(file).select { |line| line =~ definition_line_regexp }
    filestr = File.read(dname + fname)
    filecont = Regexp.new("(# RAKE AGILEDOX RESULTS BEGIN.*# RAKE AGILEDOX RESULTS END[\n])?(.*)", Regexp::MULTILINE).match(filestr)[2]
    filestr = "# RAKE AGILEDOX RESULTS BEGIN\n"
    definitions.each do |definition|
      m = definition_substring_regexp.match(definition)
      filestr += "# - " + m[definition_substring_index].gsub(/_/," ") + "\n"
    end
    filestr += "# RAKE AGILEDOX RESULTS END\n"
    File.open(dname + fname, "w") { |file|
      file << filestr << filecont
    }
  end
end

- Al Chou

I’ve added namespace support.

/app/controller/admin/


def process_test_or_spec_files( file_type )
  case file_type
    when 'test'
      definition_line_regexp = /^\s*def\s+test.*/
      definition_substring_regexp = %r"test_(should_)?(.*)" 
      definition_substring_index = 2
    when 'spec'
      definition_line_regexp = /^\s*it\s+(['"]).*\1\s+do.*/
      definition_substring_regexp = /it\s+(['"])(should )?(.*?)\1\s+do/
      definition_substring_index = 3
  end

  files = FileList["#{file_type}/**/*_#{file_type}.rb"]
  files.each do |file|
    
    if file.count("/") == 3
      dname = file.split(File::basename(file))[0]
      namespace = file.split("/")[2]
    else
      dname = File::dirname(file)
    end
   
    case file_type
      when 'test'
        case dname
          when "#{file_type}/unit" 
            dname = 'app/models/'
          when "#{file_type}/functional" 
            dname = 'app/controllers/'
	  when "#{file_type}/functional/#{namespace}/" 
            dname = "app/controllers/#{namespace}/"
	  else
            next
        end
      when 'spec'
        case dname
          when "#{file_type}/models" 
            dname = 'app/models/'
          when "#{file_type}/controllers" 
            dname = 'app/controllers/'
          when "#{file_type}/controllers/#{namespace}/"
            dname = "app/controllers/#{namespace}/"
	  when "#{file_type}/views" 
            dname = 'app/views/'
          else
            next
        end
    end
    fname = File::basename(file, "_#{file_type}.rb") + '.rb'
    unless File.exist?(dname + fname) then next end
    puts "processing " + file + "\n" 
    definitions = File::readlines(file).select { |line| line =~ definition_line_regexp }
    filestr = File.read(dname + fname)
    filecont = Regexp.new("(# RAKE AGILEDOX RESULTS BEGIN.*# RAKE AGILEDOX RESULTS END[\n])?(.*)", Regexp::MULTILINE).match(filestr)[2]
    filestr = "# RAKE AGILEDOX RESULTS BEGIN\n" 
    definitions.each do |definition|
      m = definition_substring_regexp.match(definition)
      filestr += "# - " + m[definition_substring_index].gsub(/_/," ") + "\n" 
    end
    filestr += "# RAKE AGILEDOX RESULTS END\n" 
    File.open(dname + fname, "w") { |file|
      file << filestr << filecont
    }
  end
end

http://code.google.com/p/agiledox-rake/

repository for agiledox rake task with test/spec + writing(optional) + nested actions(optional) support