skydeck

Already a member? Login.

Skydeck Blog

Unit test in OCaml with OUnit

It’s pretty easy to get overly confident about the correctness of your code when you program in OCaml. After all, the strict type checking eliminates whole classes of bugs at compile time; you never fight them because your code never builds with them. Even so, I still love running automated tests. The compiler can’t catch all boundary conditions and it certainly can’t verify any situation that is dynamically built at runtime.

At Skydeck, we use OUnit for our unit tests. It’s really straightforward to use because, not surprisingly, it follows the typical n-unit pattern: set-up and tear-down test cases, implement individual tests, and gather them all into suites for execution. Similar to other implementations — cppunit for example — you need to write your own test suites with OUnit. It’s repetitive work, ripe for automation and almost makes you miss reflection (but not quite). So we threw together a module to automate this and hooked it into ocamlbuild. Now adding a new test is simple: we just write new test functions and OCaml does the rest. I love being able to add tests quickly even more than I love that feeling when all the tests pass.

Below is the code that generates the test suite. If you’re using ocamlbuild and want to integrate with it, I’ve included the relevant sections from our myocamlbuild.ml too.
make_suite.ml:



(* This code is provided for the public domain without copyright.

   To build:

   ocamlfind ocamlc -g -package pcre -linkpkg -o make_suite \
     make_suite.ml *)

let output_tests ch tests =
  let os = output_string ch in
  os "(* This file is autogenerated.  Do not modify *)\n";
  os "open OUnit\n";
  os "\n";
  os ("let suite = \"Test Suite\" >::: [\n");
  let print_testentry s =
    Printf.fprintf ch "  \"%s\" >:: %s;\n" s s;
  in
    List.iter print_testentry tests;
  os "];;\n";
  os "\n";
  os "run_test_tt_main suite"

let module_of_filename fn =
  let fn = Filename.chop_suffix fn ".ml" in
  let fn_parts = Pcre.split ~pat:"/" fn in
  let module_parts = List.map String.capitalize fn_parts in
  String.concat "." module_parts

let with_file_in fn f =
  let ch = open_in fn in
  let r = f ch in
  close_in ch;
  r

let find_tests filenames =
  let file tests fn =
    let mn = module_of_filename fn in
    let rec line tests ch =
      try
        let l = input_line ch in
        match Pcre.extract ~pat:"^let (test_[A-Za-z0-9-_]+)" l with
          | [| _; tn |] ->
              let test = mn ^ "." ^ tn in
              line (tests @ [test]) ch
          | _ ->
              line tests ch
      with
        | Not_found -> line tests ch
        | End_of_file -> close_in ch; tests in
    with_file_in fn (line tests) in
  List.fold_left file [] filenames

let doit () =
  let output_file = ref None in
  let files = ref [] in
  Arg.parse
    [("-o",
      Arg.String (fun s -> output_file := Some s),
      "Name of output file (stdout if none given)")]
    (fun fn -> files := !files @ [fn])
    "make_suite [-o outfile] infile [infile ...]";
  let tests = find_tests !files in
  let ch =
    match !output_file with
        Some fn -> open_out fn
      | None -> stdout in
  output_tests ch tests;;

doit ()

myocamlbuild.ml:

open Ocamlbuild_plugin
open Command
module PN = Pathname

let split path =
  let rec aux path =
    if path = Filename.current_dir_name then []
    else (Filename.basename path) :: aux (Filename.dirname path)
  in List.rev (aux path)

(* I tried to use Pcre here but I don't see how to specify libs for
  myocamlbuild.ml *)

let ends_with ew s =
  let sl = String.length s in
  let ewl = String.length ew in
  sl >= ewl && String.sub s (sl - ewl) ewl = ew

let starts_with sw s =
  let sl = String.length s in
  let swl = String.length sw in
  sl >= swl && String.sub s 0 swl = sw

let rec find pred sofar fn =
  if starts_with "_build" fn || ends_with ".svn" fn
  then sofar
  else begin
    if PN.is_directory fn
    then
      let fns = PN.readdir fn in
      let fns = Array.map (PN.concat fn) fns in
      Array.fold_left (find pred) sofar fns
    else if pred fn then fn::sofar else sofar
  end

(* relative to _build; maybe should install tools somewhere? *)
let make_suite = A"../../tools/make_suite"

let make_tests files =
  rule "make tests"
    ~prod:"tests.ml"
    ~deps:files
    begin fun _ _ ->
        Cmd (S ([make_suite; A"-o"; Px"tests.ml"] @
               (List.map (fun s -> P s) files)));
    end

dispatch begin function
  | After_rules ->
      let is_test_fn fn =
        starts_with "test_" (Filename.basename fn) ||
          List.mem "test" (split (Filename.dirname fn))
      in
      let files = find is_test_fn [] "." in
      make_tests files;
  | _ -> ()
end

This document was generated using caml2html

4 Responses to “Unit test in OCaml with OUnit”

  1. Alfie Barr says:

    Can you give examples of input files to be used with your make_suite code?

  2. khigia says:

    Alfie Barr: the files in input of make_suite are regular ocaml source code, containing functions whose name begin with “test”, requiring no argument and containing any OUnit tests, like following example:
    let test_my_condition () =
    let s = “not empty string” in
    OUnit.assert_string s

  3. khigia says:

    Thanks for this code, greatly appreciated!

    I have been using some other plugin to build make_suite itself (to put make_suite in the source of my project, not as external tools).

    But then, it was difficult to add rule in ocamlbuild to first build the tool, then use it to create the executable to run the suite.

    Thus I merge both code (make_suite and your ocamlbuild plugin) to create run_test.ml … which does make_suite + actions of ocamlbuild plugin.

    Not bad, but of course it doesn’t enable to share the make_suite tools between project … however we could share a library which ease to write the run_suite in each project.

    Anyway, main point is: thanks for code and inspiration!

  4. khigia says:

    Ooops! my previous comment is not clear … my run_test do not run the test (no dynamic evaluation) but embed the code from myocamlbuild to search for files … that’s all!