.. _writing-tests: ************************* Writing and Running Tests ************************* .. TODO(diataxis) This is mostly a guide, or rather several of them. There is also some reference in it. Something we can do is split this into: - one how-to guide per test technique - a "choosing a test technique" how-to guide - reference for ``inline_tests.backend`` - reference for cram tests Dune tries to streamline the testing story as much as possible, so you can focus on the tests themselves and not bother with setting up various test frameworks. In this section, we'll explain the workflow to deal with tests in Dune. In particular, we'll see how to run the test suite of a project, how to describe your tests to Dune, and how to promote test results as expectation. We distinguish three kinds of tests: * Inline tests - written directly inside the ``.ml`` files of a library * Custom tests - run an executable, possibly followed by an action such as diffing the produced output. * Cram tests - expect tests written in Cram_ style. Running Tests ============= Whatever the tests of a project are, the usual way to run tests with Dune is to call ``dune runtest`` from the shell (or the command alias ``dune test``). This will run all the tests defined in the current directory and any subdirectory recursively. Note that in any case, ``dune runtest`` is simply shorthand for building the ``runtest`` alias, so you can always ask Dune to run the tests in conjunction with other targets by passing ``@runtest`` to ``dune build``. For instance: .. code:: console $ dune build @install @runtest $ dune build @install @test/runtest Running a Single Test --------------------- If you would only like to run a single test for your project, you may use ``dune exec`` to run the test executable (for the sake of this example, ``project/tests/myTest.ml``): .. code:: console $ dune exec project/tests/myTest.exe To run :ref:`cram-tests` you can pass their paths to the ``dune test`` command. .. code:: console $ dune test tests/myCramTest.t This works both for directory and file cram tests. Running Tests in a Directory ---------------------------- You can also pass a directory argument to run the tests from a subtree. For instance, ``dune runtest test`` will only run the tests from the ``test`` directory and any subdirectory of ``test`` recursively. .. _inline_tests: Inline Tests ============ There are several inline tests frameworks available for OCaml, such as ppx_inline_test_ and qtest_. We will use ppx_inline_test_ as an example because it has the necessary setup to be used with Dune out of the box. ppx_inline_test_ allows one to write tests directly inside ``.ml`` files as follows: .. code:: ocaml let rec fact n = if n = 1 then 1 else n * fact (n - 1) let%test _ = fact 5 = 120 The file must be preprocessed with the ``ppx_inline_test`` PPX rewriter, so for instance the ``dune`` file might look like this: .. code:: dune (library (name foo) (preprocess (pps ppx_inline_test))) In order to tell Dune that our library contains inline tests, we have to add an ``inline_tests`` field: .. code:: dune (library (name foo) (inline_tests) (preprocess (pps ppx_inline_test))) We can now build and execute this test by running ``dune runtest``. For instance, if we make the test fail by replacing ``120`` by ``0`` we get: .. code:: console $ dune runtest [...] File "src/fact.ml", line 3, characters 0-25: <<(fact 5) = 0>> is false. FAILED 1 / 1 tests Every inline test library generates an alias with the library name prefixed by `runtest-`. You can build the specific inline test library by running ``dune build @runtest-foo`` in this case. Note that in this case Dune knew how to build and run the tests without any special configuration. This is because ``ppx_inline_test`` defines an inline tests backend that's used by the library. Some other frameworks, such as qtest_, don't have any special library or PPX rewriter. To use such a framework, you must tell Dune about it, as it cannot guess. You can do that by adding a ``backend`` field: .. code:: dune (library (name foo) (inline_tests (backend qtest.lib))) In the example above, the name `qtest.lib` comes from the `public_name` field in `qtest`'s own `dune` file. Note that using ``ppx_inline_test`` requires that the opam package ``ppx_inline_test`` be installed in your switch. If you use ``ppx_inline_test`` in a package then that package must `unconditionally` depend on ``ppx_inline_test`` (ie. ``ppx_inline_test`` can't be a ``with-test`` dependency). Inline Expectation Tests ------------------------ Inline expectation tests are a special case of inline tests where written OCaml code prints something followed by what you expect this code to print. For instance, using ppx_expect_: .. code:: ocaml let%expect_test _ = print_endline "Hello, world!"; [%expect{| Hello, world! |}] The test procedure consist of executing the OCaml code and replacing the contents of the ``[%expect]`` extension point by the real output. You then get a new file that you can compare to the original source file. Expectation tests are a neat way to write tests as the following test elements are clearly identified: - The code of the test - The test expectation - The test outcome You can have a look at `this blog post `_ to find out more about expectation tests. To Dune, the workflow for expectation tests is always as follows: - Write the test with some empty expect nodes in it - Run the tests - Check the suggested correction and promote it as the original source file if you are happy with it Dune makes this workflow very easy. Simply add ``ppx_expect`` to your list of PPX rewriters as follows: .. code:: dune (library (name foo) (inline_tests) (preprocess (pps ppx_expect))) Then calling ``dune runtest`` will run these tests, and in case of mismatch, Dune will print a diff of the original source file and the suggested correction. For instance: .. code:: console $ dune runtest [...] -src/fact.ml +src/fact.ml.corrected File "src/fact.ml", line 5, characters 0-1: let rec fact n = if n = 1 then 1 else n * fact (n - 1) let%expect_test _ = print_int (fact 5); - [%expect] + [%expect{| 120 |}] In order to accept the correction, simply run: .. code:: console $ dune promote You can also make Dune automatically accept the correction after running the tests by typing: .. code:: console $ dune runtest --auto-promote Finally, some editor integration can make the editor do the promotion, which in turn makes the workflow even smoother. Running a Subset of the Test Suite ---------------------------------- You may also run a group of tests located under a directory with: .. code:: console $ dune runtest mylib/tests The above command will run all tests defined in tests and its subdirectories. Running Tests in Bytecode or JavaScript --------------------------------------- By default, Dune runs inline tests in native mode, unless native compilation isn't available. In which case, it runs them in bytecode. You can change this setting to choose the modes that tests should run in. To do this, add a ``modes`` field to the ``inline_tests`` field. Available modes are: - ``byte`` for running tests in byte code - ``native`` for running tests in native mode - ``best`` for running tests in native mode with fallback to byte code, if native compilation is not available - ``js`` for running tests in JavaScript using Node.js - ``wasm`` for running tests in Wasm using Node.js For instance: .. code:: ocaml (library (name foo) (inline_tests (modes byte best js wasm)) (preprocess (pps ppx_expect))) Specifying Inline Test Dependencies ----------------------------------- If your tests are reading files, you must tell Dune by adding a ``deps`` field the ``inline_tests`` field. The argument of this ``deps`` field follows the usual :doc:`concepts/dependency-spec`. For instance: .. code:: ocaml (library (name foo) (inline_tests (deps data.txt)) (preprocess (pps ppx_expect))) Passing Special Arguments to the Test Runner -------------------------------------------- Under the hood, a test executable is built by Dune. Depending on the backend used, this runner might take useful command line arguments. You can specify such flags by using a ``flags`` field, such as: .. code:: ocaml (library (name foo) (inline_tests (flags (-foo bar))) (preprocess (pps ppx_expect))) The argument of the ``flags`` field follows the :doc:`reference/ordered-set-language`. Passing Special Arguments to the Test Executable ------------------------------------------------ To control how the test executable is built, it's possible to customize a subset of compilation options for an executable using the ``executable`` field. Dune gives you this ability by simply specifying command line arguments as flags. You can specify such flags by using ``flags`` field. For instance: .. code:: ocaml (library (name foo) (inline_tests (flags (-foo bar) (executable (flags (-foo bar)))) (preprocess (pps ppx_expect)))) The argument of the ``flags`` field follows the :doc:`reference/ordered-set-language`. Using Additional Libraries in the Test Runner --------------------------------------------- When tests are not part of the library code, it's possible that tests require additional libraries than the library being tested. This is the case with qtest_, as tests are written in comments. You can specify such libraries using a ``libraries`` field, such as: .. code:: ocaml (library (name foo) (inline_tests (backend qtest) (libraries bar))) Changing the Flags of the Linking Step of the Test Runner --------------------------------------------------------- You can use the ``link_flags`` field to change the linker flags passed to ``ocamlopt`` when building the test runner. By default, the linking flags are ``-linkall``. You probably want to keep ``-linkall`` as one of the new list of flags (unless you know what you are doing), forcing the linker to load your test module, since the test runner doesn't depend on anything itself. This field supports ``(:include ...)`` forms. .. code:: dune (library (name foo) (inline_tests (executable (link_flags -linkall -noautolink -cclib -Wl,-Bstatic -cclib -lm))) (preprocess (pps ppx_expect))) Defining Your Own Inline Test Backend ------------------------------------- If you are writing a test framework (or for other specific cases), you might want to define your own inline tests backend. If your framework is naturally implemented by a library or PPX rewriter that's necessary to write tests, you should define this library as a backend. Otherwise simply create an empty library with your chosen backend's name. In order to define a library as an inline tests backend, simply add an ``inline_tests.backend`` field to the library stanza. An inline tests backend is specified by four parameters: 1. How to create the test runner 2. How to build the test runner 3. How to run the test runner 4. Optionally how to run the test runner to list partitions These four parameters can be specified inside the ``inline_tests.backend`` field, which accepts the following fields: .. code:: dune (generate_runner ) (runner_libraries ()) (flags ) (list_partitions_flags ) (extends ()) For instance: ```` follows the :doc:`reference/actions/index` specification. It describes an action that should be executed in the library's directory using this backend for their tests. It's expected that the action will produce some OCaml code on its standard output. This code will constitute the test runner. The action can use the following additional variables: - ``%{library-name}`` --- the name of the library being tested - ``%{impl-files}`` --- the list of implementation files in the library, i.e., all the ``.ml`` and ``.re`` files - ``%{intf-files}`` --- the list of interface files in the library, i.e., all the ``.mli`` and ``.rei`` files The ``runner_libraries`` field specifies what OCaml libraries the test runner uses. For instance, if the ``generate_runner`` actions generates something like ``My_test_framework.runtests ()``, then you should probably put ``my_test_framework`` in the ``runner_libraries`` field. If your test runner needs specific flags, you should pass them in the ``flags`` field. You can use the ``%{library-name}`` variable in this field. If your test runner supports test partitions, you should pass the flags necessary for listing partitions in the ``list_partitions_flags`` field. In such scenario, the ``flags`` field will also accepts a ``%{partition}`` variable. Finally, a backend can be an extension of another backend. In this case, you must specify this in the ``extends`` field. For instance, ppx_expect_ is an extension of ppx_inline_test_. It's possible to use a backend with several extensions in a library; however, there must be exactly one *root backend*, i.e., exactly one backend that isn't an extension of another one. When using a backend with extensions, the various fields are simply concatenated. The order in which they are concatenated is unspecified; however, if a backend ``b`` extends a backend ``a``, then ``a`` will always come before ``b``. Example of Backend ~~~~~~~~~~~~~~~~~~ In this example, we put tests in comments of the form: .. code:: ocaml (*TEST: assert (fact 5 = 120) *) The backend for such a framework looks like this: .. code:: dune (library (name simple_tests) (inline_tests.backend (generate_runner (run sed "s/(\\*TEST:\\(.*\\)\\*)/let () = \\1;;/" %{impl-files})))) Now all you have to do is write ``(inline_tests ((backend simple_tests)))`` wherever you want to write such tests. Note that this is only an example. We don't recommend using ``sed`` in your build, as this would cause portability problems. Custom Tests ============ We said in `Running tests`_ that to run tests, Dune simply builds the ``runtest`` alias. As a result, you simply need to add an action to this alias in any directory in order to define custom tests. For instance, if you have a binary ``tests.exe`` that you want to run as part of running your test suite, simply add this to a ``dune`` file: .. code:: dune (rule (alias runtest) (action (run ./tests.exe))) Hence to define a test, a pair of alias and executable stanzas are required. To simplify this common pattern, Dune provides a :ref:`tests-stanza` stanza to define multiple tests and their aliases at once: .. code:: dune (tests (names test1 test2)) Diffing the Result ------------------ It's often the case that we want to compare the actual output of a test to an expected one. For that, Dune offers the ``diff`` command, which in essence is the same as running the ``diff`` tool, except that it's more integrated in Dune, especially with the ``promote`` command. For instance, let's consider this test: .. code:: dune (rule (with-stdout-to tests.output (run ./tests.exe))) (rule (alias runtest) (action (diff tests.expected tests.output))) After having run ``tests.exe`` and dumping its output to ``tests.output``, Dune will compare the latter to ``tests.expected``. In case of mismatch, Dune will print a diff and then the ``dune promote`` command can be used to copy over the generated ``test.output`` file to ``tests.expected`` in the source tree. Alternatively, the :ref:`tests-stanza` also supports this style of tests. .. code:: dune (tests (names tests)) Dune expects the existence of a ``tests.expected`` file to infer that this is an expected test. This provides a nice way of dealing with the usual *write code*, *run*, and *promote* cycle of testing. For instance: .. code:: console $ dune runtest [...] -tests.expected +tests.output File "tests.expected", line 1, characters 0-1: -Hello, world! +Good bye! $ dune promote Promoting _build/default/tests.output to tests.expected. Note that if available, the diffing is done using the patdiff_ tool, which displays nicer looking diffs than the standard ``diff`` tool. You can change that by passing ``--diff-command CMD`` to Dune. .. _cram-tests: Cram Tests ========== Cram tests are expectation tests written in a shell-like syntax. They are ideal for testing binaries. Cram tests are automatically discovered from files or directories with a ``.t`` extension. By default, this has been enabled since Dune 3.0. For older versions, it must be manually enabled in the ``dune-project`` file: .. code:: dune (lang dune 2.7) (cram enable) File Tests ---------- To define a standalone test, we create a ``.t`` file. For example, ``foo.t``: .. code:: cram Simplest possible Cram test $ echo "testing" This simple example demonstrates two components of Cram tests: comments and commands. See :doc:`reference/cram` for a description of the syntax. To run the test and promote the results: .. code:: console $ dune runtest $ dune promote We now see the output of the command: .. code:: cram Simplest possible cram test $ echo "testing" testing This is the main advantage of expect tests. We don't need to write assertions manually; instead we detect failure when the command produces a different output than what is recorded in the test script. For example, here's an example of how we'd test the ``wc`` utility. ``wc.t``: .. code:: cram We create a test artifact called "foo" $ cat >foo < foo > bar > baz > EOF After creating the fixture, we want to verify that ``wc`` gives us the right result: $ wc -l foo | awk '{ print $1 }' 4 The above example uses the doc syntax, piping the subsequent lines to ``cat``. This is convenient for creating small test artifacts. Directory Tests --------------- In the above example we used ``cat`` to create the test artifact, but what if there are too many artifacts to comfortably fit in test file? Or some of the artifacts are binary? It's possible to include the artifacts as normal files or directories, provided the test is defined as a directory. The name of the test directory must end with ``.t`` and must include a ``run.t`` as the test script. Everything else in that directory is treated as raw data for the test. It's not possible to define rules using ``dune`` files in such a directory. We convert the ``wc`` test above into a directory test ``wc.t``: .. code:: console $ ls wc.t run.t foo.txt bar/ This defines a directory test ``wc.t`` which must include a ``run.t`` file as the test script, with ``fool.txt`` and ``bar`` are test artifacts. We may then access their contents in the test script ``run.t``: .. code:: cram Testing wc: $ wc -l foo | awk '{ print $1 }' 4 $ wc -l $(ls bar) | awk '{ print $1 }' 1231 .. seealso:: :doc:`(cram) stanza reference ` Testing an OCaml Program ------------------------ The most common testing situation involves testing an executable that is defined in Dune. For example: .. code:: dune (executable (name wc) (public_name wc)) To use this binary in the Cram test, we should depend on the binary in the test: .. code:: (cram (deps %{bin:wc})) Sandboxing ---------- Since Cram tests often create intermediate artifacts, it's important that Cram tests are executed in a clean environment. This is why all Cram tests are sandboxed. To respect sandboxing, every test should specify dependency on any artifact that might rely on using the ``deps`` field. See :doc:`concepts/sandboxing` for details about the sandboxing mechanism. Test Output Sanitation ---------------------- In some situations, Cram tests emit non portable or non-deterministic output. We recommend sanitising such outputs using pipes. For example, we can scrub the OCaml magic number using ``sed`` as follows: .. code:: console $ ocamlc -config | grep "cmi_magic_number:" | sed 's/Caml.*/$SPECIAL_CODE/' cmi_magic_number: $SPECIAL_CODE By default, Dune will scrub some paths from the output of the tests. The default list of paths is: - The ``PWD`` of the test will be replaced by ``$TESTCASE_ROOT`` - The temporary directory for the current script will be replaced by ``$TMPDIR`` To add additional paths to this sanitation mechanism, it's sufficient to modify the standard BUILD_PATH_PREFIX_MAP_ environment variable. For example: .. code:: console $ export BUILD_PATH_PREFIX_MAP="HOME=$HOME:$BUILD_PATH_PREFIX_MAP" $ echo $HOME $HOME Note: Unlike Dune's version of Cram, the original specification for Cram supports regular expression and glob filtering for matching output. We chose not to implement this feature because it breaks the test, diff, and accept cycle. With regex or glob matching, the output must now be manually inspected and possibly updated. We consider the postprocessing approach described here as superior and will not introduce output matchers. .. _ppx_inline_test: https://github.com/janestreet/ppx_inline_test .. _ppx_expect: https://github.com/janestreet/ppx_expect .. _qtest: https://github.com/vincent-hugot/qtest .. _patdiff: https://github.com/janestreet/patdiff .. _cram: https://bitheap.org/cram/ .. _BUILD_PATH_PREFIX_MAP: https://reproducible-builds.org/specs/build-path-prefix-map/