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