Skip to content
kbacha edited this page Mar 23, 2013 · 23 revisions

Create a .doctest document, for example, “simple.doctest”, and begin documenting your application, like this:

Here is an example doctest file (say called simple.doctest):

This is an example test that succeeds
		>> 1 + 2
		=> 3

And here's a test that will fail
		>> 1 + 2
		=> 4

Test a some multiline statements
		>> class Person
		      attr_accessor :name
		      end
		>> Person
		=> Person

		>> p = Person.new
		>> p.name = "Tom"
		>> p.name
		=> "Tom"
doctest: you split a file into separate named tests by adding a doctest: directive
		>> 1 + 2
		=> 4

Or, put your pastes in as comments within your ruby code, say, factorial.rb, like this:

# doctest: factorial should give correct results for 0 to 2
# >> factorial(0)
# => 1
# >> factorial(1)
# => 1
#
# comments describing the parameters can be included
# this is how it works with 2:
# >> factorial(2)
# => 2
def factorial(n)
  if n == 0
    1
  else
    n * factorial(n-1)
  end
end
#
# doctest: should work for 3, too
# 
# >> factorial(3)
# => 6

When the above code is run, i.e. with the command “rubydoctest factorial.rb”, it shows the following results:

=== Testing 'factorial.rb'...
 OK  | factorial should give correct results for 0 to 3
 OK  | should work for 3, too
6 comparisons | 2 doctests | 0 failures | 0 errors

You can do multiline statements like this, as long as the indentation level is greater than the >> start line (note that ‘end’ is at an unnatural indentation level):

	# >> all_true = true
	# >> 3.times do |t|
	#      all_true &&= (a[t] == b[t])
	#      end
	# >> all_true
	# => true

Here’s a more advanced usage example, in a Rails model, book.rb:

require File.join(File.dirname(__FILE__), "..", "rubydoctest_helper")

class Book < ActiveRecord::Base
  # === Description
  # Extension for sections and chapters collections
  #   e.g. book.sections.approved_only, book.chapters.dirty!
  # 
  # === Tests
  # doctest: Sample data should provide an example of a book with both approved
  #          and unapproved sections.
  # >> v_a_books = Book.varied(:approved)
  # >> v_a_books.size > 0
  # => true
  # 
  # doctest: Sample data should provide an example of a book with both dirty
  #          and non-dirty sections.
  # >> v_d_books = Book.varied(:dirty)
  # >> v_d_books.size > 0
  # => true
  module SectionExtension
    # === Tests
    # doctest: Book#sections.approved_only should return approved sections,
    #          including first-level sections, i.e "chapters".
    # >> s = v_a_books.first.sections.approved_only
    # >> s.size > 1
    # => true
    # >> s.any?{ |t| t.approved? }
    # => true
    # >> s.any?{ |t| !t.approved? }
    # => false
    def approved_only
      self.select{ |s| s.approved? }
    end

    # === Tests
    # doctest: Book#sections.unapproved_only should return unapproved sections,
    #          including first-level sections, i.e "chapters".
    # >> s = v_a_books.first.sections.unapproved_only
    # >> s.size > 1
    # => true
    # >> s.any?{ |t| t.approved? }
    # => false
    # >> s.any?{ |t| !t.approved? }
    # => true
    def unapproved_only
      self.select{ |s| !s.approved? }
    end

    # === Tests
    # doctest: Book#sections.dirty! should set the dirty flag for sections
    #          specific to the particular book it was called on.
    # >> book = v_d_books.first
    # >> (yes = book.sections.count(:conditions => ["dirty = ?", true])) > 0
    # => true
    # >> (no = book.sections.count(:conditions => ["dirty = ?", false])) > 0
    # => true
    # >> v_d_books.first.sections.dirty!
    # >> book.sections.count(:conditions => ["dirty = ?", true])
    # => yes + no
    # >> book.sections.count(:conditions => ["dirty = ?", false])
    # => 0
    def dirty!
      self.update_all ["dirty = ?", true]
    end
  end

  has_many :sections,  :extend => SectionExtension

  # === Description
  # Find books with a diverse set of sections with a certain property.
  # 
  # === Examples
  # @see SectionExtension module
  def self.varied(field, min = 2)
    find_by_sql \
      "SELECT * FROM books WHERE id IN
        (SELECT book_id FROM sections
         GROUP BY book_id
         HAVING COUNT(DISTINCT #{field}) >= #{min})"
  end
end

See the Rails Usage page for more information about the rubydoctest_helper script mentioned above.

In each case, you run your tests using the rubydoctest command-line tool, e.g.:

$ rubydoctest doc/sample.doctest
$ rubydoctest factorial.rb
$ rubydoctest app/models/book.rb

Note also that you can use =begin and =end to suround the tests, ex: factorial.rb

def factorial(n)
  if n == 0
    1
  else
    n * factorial(n-1)
  end
end
=begin
doctest: fact
>> factorial(0)
=> 1
=end

And that tests can be interspersed with comments, a la

# doctest: factial 0 is 1
# >> factorial(0)
# => 1
#
# These numbers grow much larger with a higher n:
#
# >> factorial(10)
# => 3628800

And since your test code and comments will appear in the rdocs, you’ve made instant documentation for parameters to your functions.

And all it cost was a copy and past from an irb session.