Published on

Unit testing OCaml code with ppx_expect: some notes

Authors

ppx_expect allows you to write inline tests which sit with your code and can be run easily with dune.

It works is a rather novel way: you print out a string in your test and then assert against the printed result.

This has the advantage of allowing you to build your tests retrospectively (inverting TDD): you can write the code that generates the test and then update it to the correct value. This can be much faster that trying to guess the output and refactoring it to work.

Things to know using ppx_expect

  • It only works on libraries, not executables. If you're building your code with dune, move the code to be tested into a (library (name my_library)...) declaration, and then reference that library in your executable as (executable ... (libraries my_library))
  • ppx_expect has a lot of JaneStreet dependencies, like Base and Core (something to be mindful of if you don't want them as transitive dependencies).
  • fmt is useful to assist with printing format strings and ppx_deriving with the show plugin is indispensible for printing your types.

Setup

ppx_expect has to be added as a project dependency first. If you're using dune-project to manage your opam files, add it in the depends clause like so:

(package)
  ; ...
  (depends fmt ppx_deriving ppx_expect)
  )

You also need to install it into your opam switch:

opam install ppx_expect

In your dune library (ppx_expect does not work with executables), add (inline_tests) and the (preprocess ppx_expect) reference:

(library (name my_library)
  (libraries fmt)
  (inline_tests)
  (preprocess (pps ppx_expect ppx_deriving.show))
)

Usage

Writing tests

Here is an example with a simple odd/even generator:

type odd_even = Odd | Even
[@@deriving show]

let is_odd_even x = if x mod 2 = 0 then Even else Odd

let%expect_test "is_odd_even 0" =
  let res = (is_odd_even 0) in
  Fmt.pr "%a" pp_odd_even res;
  [%expect "My_library.Even"]

let%expect_test "is_odd_even 5" =
  let res = (is_odd_even 5) in
  Fmt.pr "%a" pp_odd_even res;
  [%expect "My_library.Odd"]

let%expect_test "is_odd_even -10" =
  let res = (is_odd_even (-10)) in
  Fmt.pr "%a" pp_odd_even res;
  [%expect "My_library.Even"]

Executing test suite

We can execute our unit tests with:

dune runtest

If your tests are successful, you will see no output.

Test failures

Let's add an error test to see what that looks like:

let%expect_test "is_odd_even 13" =
  let res = (is_odd_even (13)) in
  Fmt.pr "%a" pp_odd_even res;
  [%expect "My_library.Even"]

We will now see error output:

File "my_library.ml", line 1, characters 0-0:
diff --git a/_build/default/my_library.ml b/_build/.sandbox/89010188fcdb897a907873b6c7c65c59/default/my_library.ml.corrected
index f9683ce..4093b19 100644
--- a/_build/default/my_library.ml
+++ b/_build/.sandbox/89010188fcdb897a907873b6c7c65c59/default/my_library.ml.corrected
@@ -21,4 +21,4 @@ let%expect_test "is_odd_even 5" =
 let%expect_test "is_odd_even 13" =
   let res = (is_odd_even (13)) in
   Fmt.pr "%a" pp_odd_even res;
-  [%expect "My_library.Even"]
+  [%expect "My_library.Odd"]