Giter Site home page Giter Site logo

yard-doctest's Introduction

yard-doctest Gem Version CI 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 'yard-doctest'

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). To do so, add yard-doctest as an automatically loaded plugin in your .yardops:

# .yardopts
--plugin 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, test_helper.rb in Minitest, or env.rb in Cucumber. You should require everything necessary for your examples to run there.

$ touch doctest_helper.rb
# or move it into the `support`, `spec`, or `test` directory
# 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

Exceptions

If you want to use example that raises exception, this can be achieved by specifying the correct expected value:

class Calculator
  # @example
  #   divide(1, 0) #=> raise ZeroDivisionError, "divided by 0"
  def divide(one, two)
    one / two
  end
end

The comparison of raised exceptions is being done by string containing the class and message of exceptions. With that said, you have to use the same message in expected value as the one that is used in actual.

Test helper

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:

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

def cat
  @cat ||= Cat.new
end

Hooks

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

YARD::Doctest.configure do |doctest|
  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

  doctest.after do
    # same as `before`, but runs after each example
  end

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

There is also a way to limit hooks to specific tests based on class/method name:

YARD::Doctest.configure do |doctest|
  doctest.before('MyClass') do
    # this will only be called for doctests of `MyClass` class
    # and all its methods (i.e. `MyClass.foo`, `MyClass#bar`)
  end

  doctest.after('MyClass#foo') do
    # this will only be called for doctests of `MyClass#foo`
  end

  doctest.before('MyClass#foo@Example one') do
    # this will only be called for example `Example one` of `MyClass#foo`
  end
end

Skip

You can skip running some of the tests:

YARD::Doctest.configure do |doctest|
  doctest.skip 'MyClass' # will skip doctests for `MyClass` and all its methods
  doctest.skip 'MyClass#foo' # will skip doctests for `MyClass#foo`
end

Rake

There is also a Rake task for you:

# Rakefile
require 'yard/doctest/rake'

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

Is it really used?

Well, yeah. A great example of using yard-doctest is watir-webdriver.

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.

yard-doctest's People

Contributors

jeanmertz avatar lygaret avatar michaelherold avatar nrser avatar p0deje avatar quartzmo avatar woarewe avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

yard-doctest's Issues

Advanced examples

I can find only ultra simple examples where return values tested are a boolean or a known integer. But what if I want to assert the class of an object (assert_instance_of with minitest), and operator (assert_operator with minitest).

Because most Minitest assertion are sugar that can be replaced by a more classic assert_equal, eg assert_instance_of(Array, obj, 'Message") can be replaced by assert_equal(true, obj.is_a?(Array), 'Messag') but we can't control that with yard-doctest.

So if I have a method returning a complex object, a long array or a random number I don't want to test the exact value return but if the object has a particular method, check the class, check a range, etc.

Eg. I have a method that is returning an array of CSV::Row but I can't do any test yard-doctest while I have planty options with minitest.

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

I could write true #=> obj.is_a?(Array) to have a test but this would ruin the whole point of reusing a yard example as a test and will make test example unreadable and not much of an example anymore. And that does not even work because what is after #=> is not on the same context as what is before so variables or instances are not shared anymore.

So maybe some other operators could be added for more minitest assert types coverage, eg. method #=? Array for assert_instance_of.

Thank you!

Hi @p0deje, this is not an issue exactly. I just wanted to thank you (and any other contributors) for the yard-doctest gem, which has worked super easily and smoothly for me.

It's so valuable to have test coverage of documentation code examples. I'd love to see this practice more publicized, perhaps through a blog post picked up on Ruby Weekly News?

Thanks again,
-Marc

Support examples in README.md ?

If the gem already supports testing examples in README.md, how do we set that up?

If not, since YARD does already process README.md, would it be at all possible to support this? Happy to implement it myself if guidance provided!

Feature idea: file-level filtering option

Summary

Add a CLI option to only doc-test source files containing a special comment or YARD tag.

This would allow for gradual file-by-file incorporation into existing code bases that already have @example tags all over the place that were not written to doc-test.

Like how Flow uses // @flow comments to activate type checking at a file level, if you happen to have used that. I swear I've seen this sort of thing in other softwares as well, but they're not coming to mind at the moment.

Usage

Add comments or tags to the desired file and add a flag to the CLI command, something like:

yard doctest --opt-in

I'm not sure about the name --opt-in, but I'm having trouble thinking of something better right now.

Background

I'm doing this right now by sticking a

# doctest: true

comment in the files that I've migrated over to use doc-tests, then running via a harness script

bundle exec \
  yard doctest $@ \
    $(rg --files-with-matches '# doctest: true' --glob='*.rb' ./lib/)

However, I think it would ease adoption into existing code bases to have this functionality built-in. And I'd like to see more adoption, 'cause I feel there is little more frustrating than wasting precious time with new software in "what the hell am I doing wrong?"-land only to eventually figure out that the example itself is broken.

Implementation Thoughts

Ruby already has "magic" file-level comments like # frozen_string_literal: true, etc., so there's some basis for the # doctest: true approach, though it might be confusing since the other magic comments people are used to seeing are Ruby VM directives, and this is not. But magic comments are also used to tell editors things about the file and such, so it seems reasonable. Maybe # yard-doctest: true would be more clear.

Using a YARD tag is possibly another option, like @doctest true, but it's a least a bit more complicated... I'm not familiar how (if at all) YARD binds file-level doc-strings as I've only ever used "code-object"-level stuff.

A YARD tag could be naturally be extended to finer granularity: putting @doctest true or @doctest false on method docs to switch those on or off individually.

bug with using variable

# @example Hash/Array keep the structure
#   hash = { 'foo' => 'bar' }
#   Boss.pack [hash, hash]             #=> "\x16\x0F\efoo\ebar\x15"
#   src = Boss.unpack("\x16\x0F\efoo\ebar\x15")
#   src[0]['foo'] == 'bar'
#   #=> true
#   src[1]['foo'] = 'bar'
#   #=> true

yard doctest:

1) Failure:
Boss#test_0001_Hash/Array keep the structure [/Users/sergeych/dev/boss_protocol/lib/boss-protocol.rb:90]:
--- expected
+++ actual
@@ -1 +1 @@
-true
+"#"

It is caused by the src[1] line! If I remove it, test works as expected.

Features fail on recent YARDs because they no longer auto-load plugins by default

Looks like YARD stopped auto-loading all plugins by default at 0.6.2:

https://github.com/lsegal/yard/blob/565343f35fce64df4c16ef89a2c48dcf1f93b8c3/lib/yard/config.rb#L29

Repro: Just do a clean checkout and bundle install ... (which should get you YARD 0.9.18 at the moment) then bundle exec rake and you'll see most features fail with their help message because yard won't recognize doctest.

Fix is simple, just need a .yardopts with --plugin yard-doctest, I'll put up a PR in a second.

`rake` is required although it's listed as dev dependency

Hi,

I'm getting an error when running yard doctest:

[error]: Error loading plugin 'yard-doctest'
...

Because I don't have rake installed in my project.

In gemspec rake is listed as dev dependency, although in yard-doctest.rb it requires rake.

To fix it rake should either be listed as a runtime dependency or be removed from yard-doctest.rb and only loaded directly from Rakefile

Thanks for the awesome gem ๐Ÿ‘

Deprecated warning from Minitest (5.11.3), wants `assert_nil`

Using the latest Minitest (5.11.3) and doing something like

# @example
#   foo #=> nil
# 
def foo
  nil
end

You get an output like

# Running:

........DEPRECATED: Use assert_nil if expecting nil from (...)/yard-doctest-0.1.16/lib/yard/doctest/example.rb:82. This will fail in Minitest 6.
.........

The fix is like two lines, I'll PR a feature and fix in a second.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.