Minitest extension for testing Ruby code blocks in your README and other Markdown files
_why?
Document your Gem's usage, examples etc. with fully testable code! Better still, use your README as a BDD aid and specify functionality in your README/wiki code blocks before you write your code!!
Installation
Add the gem to the application's Gemfile:
bundle add minitest-markdown
If bundler is not being used to manage dependencies, install the gem by executing:
gem install minitest-markdown
Configuration
No configuration is required if you use Bundler. If not, set your project_root path using the setter method:
@config = Minitest::Markdown.config
# => instance_of Configuration
Configuration.instance_methods(false)
# => includes :project_root=
Usage
In your test class
To test the Ruby blocks in your README file, create file test_readme.rb (for example) and add the following:
require 'minitest/autorun' # or in your test_helper
require 'minitest/markdown' # ditto
require 'minitest/hooks/test' # optional, see 'State' section below
class ReadmeTest < Minitest::Test # or your own subclass of Minitest::Test
include Minitest::Hooks # optional, see 'State' section below
Markdown.generate_markdown_tests(self)
end
# => truthy
To test Ruby blocks in another Markdown file, create another test file and pass the path to your Markdown file using the :path keyword arg i.e. Markdown.generate_markdown_tests(self, path: '/path/to/your/markdown/file.md')
In your Markdown - magic comments become assertions
Each Markdown file is represented by a single test class and each Ruby block* in a file becomes a test method with zero or more assertions. Test methods are named according to their index; test_block0, test_block1 etc.
*Any 'state' blocks are excluded from indexing - see State section below
The syntax used for assertions is # => followed by an assertion keyword. Keywords may be any one of the Minitest "assert_" assertions less the "assert_" prefix (refutations are not implemented at this time). If the keyword is omitted, the default assertion; assert_equal is used. The actual value passed to the assertion is the result of the evaluation of the Ruby code above each magic comment. The following block (a single test) includes 6 assertions:
File.read 'test/fixtures/klass.rb'
# => "class Klass\n def hello\n 'Hello Markdown!'\n end\nend"
require 'test/fixtures/klass' # a demonstration
# => true
# ordinary comments are ignored.
Klass.new # inline comments are also ignored
# The assertion and expected value are:-
# => instance_of Klass
# No keyword here, so the default assertion is used (assert_equal)
Klass.new.hello
# => 'Hello Markdown!'
Klass.hello
# => raises NoMethodError
self
# => instance_of Markdown::TestClass
Plain old assert has been aliased as assert_truthy, so when expecting a truthy value you should do this:
[1, 'true', true].sample
# => truthy
For convenience, the assertion assert_includes has also been aliased so that it operates either way around:
[1, 2, 3]
# => includes 2
2
# => included_in [1, 2, 3]
Everything on the magic comment line after the assertion keyword, or # => if one is omitted, is evaluated as Ruby code. Note: inline comments are NOT ALLOWED on magic comment lines. Where an assertion takes multiple positional args, these are simply separated by commas. Note that the assertion keyword itself is not an argument. The syntax is as follows:
22/7.0
# => in_delta Math::PI, 0.01
To skip a test, use skip, as you would in a regular test:
"some code which you don't want to test yet"
# => skip 'give a reason for the skip here'
Test failures will look like this - note the method name test_block10 in this example:
Minitest::Markdown::ReadmeTest#test_block10 [lib/minitest/markdown/test_class.rb:118]:
Expected: 42
Actual: 0
State
Instance vars are shared across test methods within a class, but as Minitest's default is to run tests in random order you may want to use a setup block in order to ensure a stored value is available to all test blocks (tests) in the Markdown file test class (see below):
@instance_var
# => 7
@before_all_instance_var # see hook methods below
# => 'foo'
Minitest's setup and teardown methods are generated by using the appropriate comment on the first line of a code block. Assertion magic comments are ignored in such blocks, as these are not tests. E.g.
# setup
# do some setup task - or:-
@instance_var = 7 # now available in all test method blocks, including the one above
# => IGNORED
# teardown
# do some teardown task
The hook methods defined in the minitest-hooks extension (before_all, after_all, around & around_all)are also available in this way if minitest-hooks is installed and Minitest::Hooks is included in your markdown test class. See the 'In your test class' section above for an example. You can now do this:
# before_all
@before_all_instance_var = 'foo'
Everything in the Ruby code blocks above and below here runs as test code. minitest-proveit would complain otherwise ;-)
Mocks
Mocks use the "mock" keyword for assert_mock in place of the equivalent mock.verify:
@mymock = Minitest::Mock.new
@mymock.expect(:puts, nil, ['Hello World!'])
@mymock.puts 'Hello World!'
# => mock @mymock
Stubbing
It is possible to pass stubs to the generated tests. This is done using the stubs keyword. Hash keys represent the index of the test code block. Important: any 'state' blocks are ignored for test code block indexing. The hash value must be an instance of Minitest::StubChain which holds a proc for each stub in the chain. For convenience StubChain.stubproc returns a suitable stub proc object which is called around the relevant test code. Zero or more of these reusable procs can be used to instantiate a StubChain object:
class MarkdownTest < Minitest::Test
set_stubs_new = Minitest::StubChain.stubproc(Set, :new, []) # returns a proc which stubs Set.new to return an empty array
array_stubs_size = Minitest::StubChain.stubproc(Array, :size, 42, any_instance: true) # uses the bundled minitest-stub_any_instance gem
stub_chain = Minitest::StubChain.new([set_stubs_new]) # initialized with zero or more stub procs
stub_chain.stubs << array_stubs_size # add another like this if need be
stubs = {}
stubs[10] = stub_chain
stubs[11] = stub_chain # StubChain instances themselves may be reused
Markdown.generate_markdown_tests(self, stubs: stubs)
end
# => truthy
The 2 stubs in the StubChain instance above are demonstrated in the following example:
# This is test_block10
Set.new([1, 'c', :s]).size
# => 42
# Because we added `stubs[10] = stub_chain` above, this is exactly equivalent to:
#
# def test_block10
# Set.stub(:new, []) do
# Array.stub_any_instance(:size, 42) do
# assert_equal 42, Set.new([1, 'c', :s]).size
# end
# end
# end
Example showing the reuse of a StubChain:
# This is test_block11
Set.new.size
# => 42
Errors
All errors subclass Markdown::Error
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitLab at https://gitlab.com/matzfan/minitest-markdown. Please checkout a suitably named branch before submitting a PR.
License
The gem is available as open source under the terms of the MIT License.