yard-doctest Gem Version Build Status

Have you ever wanted to turn your amazing code examples into something that really make sense, is always up-to-date and bullet-proof? Were looking at an amazing Python doctest? Well, look no longer!

Meet YARD::Doctest - simple and magical gem, which automatically parses your @example tags and turn them into tests!

Installation

Add this line to your application's Gemfile:

gem 'yardoctest'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install yard-doctest

Basic usage

Let's imagine you have the following library:

lib/
  cat.rb
  dog.rb

Each file contains some class and methods:

# cat.rb
class Cat
  # @example
  #   Cat.word #=> 'meow'
  def self.word
    'meow'
  end

  def initialize(can_hunt_dogs = false)
    @can_hunt_dogs = can_hunt_dogs
  end

  # @example Usual cat cannot hunt dogs
  #   cat = Cat.new
  #   cat.can_hunt_dogs? #=> false
  #
  # @example Lion can hunt dogs
  #   cat = Cat.new(true)
  #   cat.can_hunt_dogs? #=> true
  #
  # @example Mutated cat can hunt dogs too
  #   cat = Cat.new
  #   cat.instance_variable_set(:@can_hunt_dogs, true) # not part of public API
  #   cat.can_hunt_dogs? #=> true
  def can_hunt_dogs?
    @can_hunt_dogs
  end
end
# dog.rb
class Dog
  # @example
  #   Dog.word #=> 'meow'
  def self.word
    'woof'
  end

  # @example Dogs never hunt dogs
  #   dog = Dog.new
  #   dog.can_hunt_dogs? #=> false
  def can_hunt_dogs?
    false
  end
end

You can run tests for all the examples you've documented.

First of all, you need to tell YARD to automatically load yard-doctest (as well as other plugins):

$ bundle exec yard config load_plugins true
# if you don't want to load other plugins
$ bundle exec yard config -a autoload_plugins yard-doctest

Next, you'll need to create test helper, which will be required before each of your test. Think about it as spec_helper.rb in RSpec or env.rb in Cucumber. You should require everything necessary for your examples to run there.

$ touch yard-doctest_helper.rb
# yard-doctest_helper.rb
require 'lib/cat'
require 'lib/dog'

That's pretty much it, you can now run your examples:

$ bundle exec yard doctest
Run options: --seed 5974

# Running:

..F...

Finished in 0.015488s, 387.3967 runs/s, 387.3967 assertions/s.

  1) Failure:
Dog.word#test_0001_ [lib/dog.rb:5]:
Expected: "meow"
  Actual: "woof"

6 runs, 6 assertions, 1 failures, 0 errors, 0 skips

Oops, let's go back and fix the example by change "meow" to "woof" in Dog.word and re-run the examples:

$ sed -i.bak s/meow/woof/g lib/dog.rb
$ bundle exec yard doctest
Run options: --seed 51966

# Running:

......

Finished in 0.002712s, 2212.3894 runs/s, 2212.3894 assertions/s.

6 runs, 6 assertions, 0 failures, 0 errors, 0 skips

Pretty simple, ain't it? Need more details about the way it parses examples?

Think about #=> as equality assertion: everything before is actual result, everything after is expected result and they are asserted using #==.

You can use as many assertions as you want in a single example:

class Cat
  # @example
  #   cat = Cat.new
  #   cat.can_hunt_dogs? #=> false
  #   cat = Cat.new(true)
  #   cat.can_hunt_dogs? #=> true
  def can_hunt_dogs?
    @can_hunt_dogs
  end
end

In this case, example will be run as a single test but with multiple assertions:

$ bundle exec yard doctest lib/cat.rb
# ...
1 runs, 2 assertions, 0 failures, 0 errors, 0 skips

If your example has no assertions, it will still be evaluated to ensure nothing is raised at least:

class Cat
  # @example
  #   cat = Cat.new
  #   cat.can_hunt_dogs?
  def can_hunt_dogs?
    @can_hunt_dogs
  end
end
$ bundle exec yard doctest lib/cat.rb
# ...
1 runs, 0 assertions, 0 failures, 0 errors, 0 skips

Pretty simple, ain't it? Need more details about the way it runs the tests?

It is actually delegated to amazing minitest and each example is an instance of Minitest::Spec.

Advanced usage

You can define any methods and instance variables in test helper and they will be available in examples.

For example, if we change the examples for Cat#can_hunt_dogs? like that:

# cat.rb
class Cat
  # @example Usual cat cannot hunt dogs
  #   cat.can_hunt_dogs? #=> false
  def can_hunt_dogs?
    @can_hunt_dogs
  end
end

And run the examples - it will fail because cat is undefined:

$ bundle exec yard doctest
  # ...
  1) Error:
Cat#can_hunt_dogs?#test_0001_Usual cat cannot hunt dogs:
NameError: undefined local variable or method `cat' for Object:Class
  # ...

If you don't want to create new instance of class each time (or include module if you're testing it), you can fix this by defining a method in test helper:

# yard-doctest_helper.rb
require 'lib/cat'
require 'lib/dog'

def cat
  @cat ||= Cat.new
end

In case you need to do some preparations/cleanup between tests, hooks are at your service to be defined in test helper:

YARD::Doctest.before do
  # this is called before each example and
  # evaluated in the same context as example
  # (i.e. has access to the same instance variables)
end

YARD::Doctest.after do
  # same as `before`, but runs after each example
end

YARD::Doctest.after_run do
  # runs after all the examples and
  # has different context
  # (i.e. no access to instance variables)
end

There is also a Rake task for you:

# Rakefile
require 'yard-doctest'
YARD::Doctest::RakeTask.new do |task|
  task.doctest_opts = %w[-v]
  task.pattern = 'lib/**/*.rb'
end
$ bundle exec rake yard:doctest

Testing

There are some system tests implemented with Aruba:

$ bundle install
$ bundle exec rake cucumber

Contributing

  • Fork the project.
  • Make your feature addition or bug fix.
  • Add tests for it. This is important so I don't break it in a future version unintentionally.
  • Commit, do not mess with Rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
  • Send me a pull request. Bonus points for topic branches.