image:https://img.shields.io/travis/asciidoctor/asciidoctor-doctest/master.svg?style=flat[Build Status, link="https://travis-ci.org/asciidoctor/asciidoctor-doctest"] image:https://img.shields.io/codeclimate/coverage/github/asciidoctor/asciidoctor-doctest.svg?style=flat[Test Coverage, link="https://codeclimate.com/github/asciidoctor/asciidoctor-doctest"] image:https://img.shields.io/codeclimate/github/asciidoctor/asciidoctor-doctest.svg?style=flat[Code Climate, link="https://codeclimate.com/github/asciidoctor/asciidoctor-doctest"] image:https://inch-ci.org/github/asciidoctor/asciidoctor-doctest.svg?branch=master&style=flat[Inline docs, link="http://inch-ci.org/github/asciidoctor/asciidoctor-doctest"] image:https://img.shields.io/gem/v/asciidoctor-doctest.svg?style=flat[Gem Version, link="https://rubygems.org/gems/asciidoctor-doctest"] image:https://img.shields.io/badge/yard-docs-blue.svg?style=flat[Yard Docs, link="http://www.rubydoc.info/github/asciidoctor/asciidoctor-doctest/frames"]

DocTest is a tool for end-to-end testing of Asciidoctor backends based on comparing of textual output.

It provides a collection of categorized input examples (documents in AsciiDoc syntax) to simplify and systematize writing tests for new backends. You just write or generate the expected output, i.e. what the backend should produce for the given input.

diagram

Each example should be focused on one use case, so when writing a new backend, you can incrementally implement new features following the reference input examples. However, they are not strictly isolated like unit tests. For example, if you change a format of a paragraph, it may affect a variety of other examples.

When test fails, DocTest prints a nicely formatted diff of the expected and actual output (see [run-tests]), so you see exactly what went wrong. Insignificant differences, such as attributes order in HTML, are ignored.

DocTest supports HTML-based backends and can be easily extended to support any other backend with textual output.

Setup DocTest

Let’s say that you’re developing a new shiny HTML template-based backend named “shiny” and assume that you have templates in the directory data/templates.

  1. Create a directory for your output examples:

    mkdir -p test/examples/shiny

    and optionally a directory for your extra input examples:

    mkdir -p test/examples/asciidoc
  2. Add development dependency on asciidoctor-doctest to your gemspec:

    s.add_development_dependency 'asciidoctor-doctest', '~> 2.0.0'

    or Gemfile if you’re not distributing the backend as a gem:

    gem 'asciidoctor-doctest', '~> 2.0.0'

    and run bundle install.

  3. Create or edit Rakefile and configure DocTest tasks:

    require 'asciidoctor/doctest'
    require 'thread_safe'
    require 'tilt'
    
    DocTest::RakeTasks.new(:doctest) do |t|
      # Add extra input examples; if you don’t need that, you can omit this.
      t.input_examples :asciidoc, path: [
        *DocTest.examples_path,
        'test/examples/asciidoc'
      ]
      t.output_examples :html, path: 'test/examples/shiny'
      t.converter = DocTest::HTML::Converter
      t.converter_opts = {
        template_dirs: 'data/templates'
      }
    end
  4. Check if rake loads the DocTest tasks doctest, doctest:test and doctest:generate.

    bundle exec rake -D

Run tests

Assume that you have defined the Rake tasks in the default namespace doctest (see above). Then you can simply run:

bundle exec rake doctest:test

To test only specific examples, use PATTERN with glob-like expression:

bundle exec rake doctest:test PATTERN='inline_*:*'

For verbose output, use VERBOSE=yes:

bundle exec rake doctest:test VERBOSE=yes
Failing test in term

Examples

Test example is just a document fragment in AsciiDoc syntax (a reference input), or the backend’s target syntax (an expected output). Example should consider one case of the generated output, i.e. it should reflect one code branch in a converter or template. Examples are grouped in example groups. Each group focuses on one block or inline element — more precisely Asciidoctor’s AST node (paragraph, table, anchor, footnote…).

Examples group is a text file named similar to Asciidoctor templates, i.e. the AST node name with an extension according to syntax, for example table.adoc, table.html. See below for a list of the AST nodes. Individual examples in the group file are separated by a special header with the name of the example, an optional description and options.

Each example is identified by its name and the group name like this: {group_name}:{example_name} (e.g. table:with-title). Input and output examples are paired — for every input example there should be an output example with the same identifier. When you run tests, the input example is converted using the tested backend (or templates) and its actual output is compared with the expected output example.

List of Asciidoctor’s AST nodes
document

TODO

embedded

TODO

section

document sections, i.e. headings

admonition

an admonition block

audio

an audio block

colist

a code callouts list

dlist

a labeled list (aka definition list) and a Q&A style list

example

an example block

floating_title

a discrete or floating section title

image

an image block

listing

a listing and source code block

literal

a literal block

olist

an ordered list (i.e. numbered list)

open

open blocks, abstract, …

outline

an actual TOC content (i.e. list of links), usually recursively called

page_break

page break

paragraph

a paragraph

pass

a passthrough block

preamble

a preamble, optionally with a TOC

quote

a quote block

sidebar

a sidebar

stem

a STEM block (Science, Technology, Engineering and Math)

table

a table

thematic_break

a thematic break (i.e. horizontal rule)

toc

a TOC macro (i.e. manually placed TOC); This block is used for toc::[] macro only and it’s responsible just for rendering of a the TOC “envelope,” not an actual TOC content.

ulist

an unordered list (aka bullet list) and a checklist (e.g. TODO list)

verse

a verse block

video

a video block

inline_anchor

anchors (links, cross references and bibliography references)

inline_break

line break

inline_button

UI button

inline_callout

code callout icon/mark inside a code block

inline_footnote

footnote

inline_image

inline image and inline icon

inline_kbd

keyboard shortcut

inline_menu

menu section

inline_quoted

text formatting; emphasis, strong, monospaced, superscript, subscript, curved quotes and inline STEM

Input examples

DocTest provides a collection of the reference input examples that should be suitable for most backends. You can find them in data/examples/asciidoc.[1] There are a lot of test examples and some of them may not be relevant to your backend — that’s okay, when some output example is not found, it’s marked as skipped in test.

You can also write your own input examples and use them together with those provided (or replace them). Just add another directory to your examples_path (e.g. test/examples/asciidoc) and create example group files with .adoc suffix here (e.g. video.adoc). When DocTest is looking for examples to test, it indexes all examples found in files with .adoc suffix on the examples_path. If there are two files with the same name, it simply merges their content, and if they contain two examples with the same name, then the first wins (i.e. that from the file that is ahead on the examples_path).

Format

// .first-example
// Each block must be preceded by a header (comment); the first line must
// contain the example’s name prefixed with a dot. This text is interpreted
// as a description.
The example’s content in *Asciidoc*.

NOTE: The trailing new line (below this) will be removed.

// .second-example
* List item level 1
** List item level 2

HTML-based examples

HtmlTest assumes that paragraphs are enclosed in <p></p> tags and implicitly sets the include option to ./p/node() for inline_*:* examples (if include is not already set). If it’s not your case, then you must overwrite it:

DocTest::RakeTasks.new(:doctest) do |t|
  t.output_examples :html, path: 'test/examples/shiny'
  t.converter = DocTest::HTML::Converter
  t.converter_opts = {
    template_dirs: 'data/templates',
    paragraph_xpath: './div/p/node()'  //<1>
  }
end

Options

List of options that can be set in the header of HTML example.

include

XPath expression that specifies a subsection of the document that should be compared (asserted). Default is ./p/node() for inline_*:* groups and empty (i.e. .) for others.

exclude

XPath expression that specifies parts of the document that should not be compared (asserted). Always start the expression with a dot (e.g. .//h1). This option may be used multiple times per example.

header_footer

Option for Asciidoctor to render a full document (instead of embedded). This is default for document:* group.

Format

<!-- .first-example
  Each example must be preceded by a header (comment); the first line must
  contain the example’s name prefixed with a dot. This text is interpreted
  as a description.
-->
<p>The example’s content in <strong>HTML</strong>.</p>

<div class="note">The trailing new line (below this) will be removed.</div>

<!-- .second-example
  You may also specify options for comparing or Asciidoctor renderer. Option
  line starts with a semicolon, then comes the option name ended by a
  semicolon and after that the option’s value (may be omitted for boolean
  options).
  :option_1: value 1
  :option_2: value 1
  :option_2: value 2
  :boolean_option:
-->
<div class="colist">
  <ol>
    <li>Method signature</li>
    <li>Some stuff inside</li>
    <li>Return statement</li>
  </ol>
</div>

Generate examples

Writing examples of an expected output for all the input examples from scratch is quite a chore. Therefore DocTest provides a generator. When you have at least partially working Asciidoctor backend (converter or a set of templates), you can pass the input examples through it and generate your output examples. Then you should verify them and modify if needed.

Assume that you have defined the Rake tasks in the default namespace doctest (see [setup-doctest]).

Now you can generate output examples from all the input examples (those with .adoc extension) found on the examples_path that doesn’t already exist (i.e. it doesn’t rewrite existing):

bundle exec rake doctest:generate

Same as previous, but rewrite existing tested examples:

bundle exec rake doctest:generate FORCE=yes

Generate just examples for ulist node (i.e. all examples in ulist.adoc file(s) found on the examples_path) that doesn’t exist yet:

bundle exec rake doctest:generate PATTERN='ulist:*'

(Re)generate examples which name starts with basic for all inline nodes (i.e. files that starts with inline_):

bundle exec rake doctest:generate PATTERN='inline_*:basic*' FORCE=yes

How to extend it

You can extend DocTest to support any textual format you want. All what you need is to subclass IO::Base and AsciidocConverter (see HTML::Converter for example). Please note that this code is still in beta – refactoring is not finished, so some parts of the doctest code-base are kinda strange now (i.e. AsciidocConverter and HTML::Converter).

Contributing

  1. Fork it

  2. Create your feature branch (git checkout -b my-new-feature)

  3. Commit your changes (git commit -am 'Add some feature')

  4. Push to the branch (git push origin my-new-feature)

  5. Create new Pull Request

License

This project is licensed under MIT License. For the full text of the license, see the LICENSE file.


1. Since GitHub implicitly renders them as a plain AsciiDoc, you must view a Raw source if you want to see what’s going on there.