View Source doctest

A library to test Erlang -moduledoc and -doc attributes.

Requirements

OTP >= 27.

Installation

% rebar.config
{profiles, [
    {test, [
        {deps, [{doctest, "0.8.0"}]}
    ]}
]}.

Usage

Tests run via the doctest:module/1,2 function or on modules that include the doctest header, but only exported functions are tested.

Testing via doctest:module/1,2 function

Take this module:

-module(foo).
-moduledoc """
Module doc tags can also be tested.

```erlang
1> foo:foo() =:= bar.
true
```
""".

-export([foo/0]).

-doc """
```erlang
1> foo:foo().
foo
```
""".
foo() ->
    bar.

Running it via rebar3 as test shell:

1> doctest:module(foo).
 PASS  ./test/support/foo.erl:6 -moduledoc
 FAIL  ./test/support/foo.erl:15 -doc

     assertEqual

    Expected: foo
    Received: bar

    
 15  1> foo:foo().
 16  foo
    
     at ./test/support/foo.erl:15

Tests: 1 failed, 1 passed, 2 total
 Time: 0.007 seconds

Options

Options can be provided when using the doctest:module/2 function. The available options are:

  • moduledoc :: boolean(): enable or disable -moduledoc test
  • funs :: boolean() | [{atom(), arity()}]: enable or disable -doc tests or define the functions to be tested

  • eunit :: resolve | term(): set EUnit options

Testing via doctest header

Take this module:

-module(math).

-export([add/2]).

-ifdef(TEST).
-include_lib("doctest/include/doctest.hrl").
% -doctest <see the options section>.
-endif.

-doc """
Adds two numbers together.

_Example_:
```erlang
1> math:add(0, 1).
1
2> math:add(
.. 1,
.. 1
.. ).
2
```
""".
add(A, B) ->
    A+B.

Note that the code is defined like in the Erlang shell, starting the expression line from 1..N and the ^([1-9][0-9]*)>\s format, and continues in multiple lines with .. and the format ^(\s*)\.\.\s. The expression header and multiple lines must be aligned, for example:

% Valid
1> foo.
foo
2> foo
.. =:=
.. bar.
bar

% Invalid
1> foo.
foo
200> foo % <- Must 2 (previous line + 1)
 .. =:=  % <- Must be aligned
  .. bar.
bar

Now, by running rebar3 eunit:

 PASS  ./src/math.erl:29 -doc
 PASS  ./src/math.erl:31 -doc


Tests: 2 passed, 2 total
 Time: 0.008 seconds

By changing the first test to:

1> math:add(1, 1).
1

And running rebar3 eunit again:

 FAIL  ./src/math.erl:15 -doc

     assertEqual

    Expected: 1
    Received: 2

    
 15  1> math:add(1, 1).
 16  1
    
     at ./src/math.erl:15

 PASS  ./src/math.erl:17 -doc


Tests: 1 failed, 1 passed, 2 total
 Time: 0.007 seconds

Options

Options are defined via the -doctest attribute and can be defined multiple times. The available options are:

  • boolean() | {enabled, boolean()}: enable or disable the test running, e.g.:

    -doctest true.
  • {moduledoc, boolean()}: enable or disable -moduledoc test, e.g.:
    -doctest {moduledoc, true}.
  • [{atom(), arity()}] | {funs, [{atom(), arity()}] | boolean()}: enable or disable -doc tests or define the functions to be tested, e.g.:

    -doctest [add/2].
  • eunit :: resolve | term(): set EUnit options, e.g.:

    -doctest {eunit, resolve}.
  • map(): define all or partial options, e.g.:
    -doctest #{
        enabled => true,
        moduledoc => true,
        funs => [add/2],
        eunit => resolve
    }.

Global options

Options can be globally defined via a config file, e.g.:

% config/sys.config
[{doctest, [
    {enabled, true},
    {moduledoc, false},
    {funs, true},
    {eunit, [no_tty, {report, {eunit_progress, [colored, profile]}}]}
]}].

EUnit options

Valid EUnit options are the resolve atom and a proplist.

By defining resolve as the EUnit options, doctest will try to resolve the options via rebar. Custom options can be defined as documented at rebar3.org:

The default EUnit options can be configured, as documented here.

Interesting undocumented options are:

  • no_tty completely disables the default EUnit reporter output
  • {report, {Module, Args}} runs a custom EUnit reporter (the functionality that prints results to the shell). The reporter module needs the following callbacks implemented:
    -export([start/0]).
    -export([start/1]).
    -export([init/1]).
    -export([handle_begin/3]).
    -export([handle_end/3]).
    -export([handle_cancel/3]).
    -export([terminate/2]).

no_tty and report can be combined to replace the EUnit reporter with a custom one:

{eunit_opts, [no_tty, {report, {my_reporter, Opts}}]}.

Doctest EUnit Reporter

doctest has a built-in EUnit reporter called doctest_eunit_report for better visualization of the tests. It can be used by defining the below option in the rebar.config of your project:

{eunit_opts, [no_tty, {report, {doctest_eunit_report, []}}]}.

Currently, no options are expected/provided by the reporter.

An example of the doctest_eunit_report output: doctest_eunit_report

TODO

  • [ ] More tests
  • [ ] Specs
  • [ ] Improve docs

Sponsors

If you like this tool, please consider sponsoring me. I'm thankful for your never-ending support :heart:

I also accept coffees :coffee:

"Buy Me A Coffee"

Contributing

Issues

Feel free to submit an issue on Github.

License

Copyright (c) 2024 William Fank Thomé

doctest is 100% open source and community-driven. All components are available under the Apache 2 License on GitHub.

See LICENSE.md for more information.