Unit tests using modules

So far, we have implemented unit-test functions for OCaml functions. But now we define OCaml functions in OCaml modules. The goal of this lecture note is to evolve our unit-test paraphernalia and implement unit-test functors for OCaml modules.

Environments

In the lecture note about Environments, revisited, we defined environments as an abstract data type with the following signature:

type name = string;;

module type ENVIRONMENT =
  sig
    type 'a environment
    val empty : 'a environment
    val extend : name -> 'a -> 'a environment -> 'a environment
    val lookup : name -> 'a environment -> 'a option
  end;;

Unit-test functions

Still in the lecture note about Environments, revisited, we declared a unit-test function to illustrate the properties expected from a correct implementation of environments:

let test_environment_int empty extend lookup =
  let b0 = (lookup "x" empty
            = None)
  and b1 = (lookup "x" (extend "x" 2 empty)
            = Some 2)
  and b2 = (lookup "x" (extend "x" 2 (extend "x" 3 empty))
            = Some 2)
  and b3 = (lookup "x" (extend "y" 2 (extend "x" 3 empty))
            = Some 3)
  and b4 = (lookup "z" (extend "y" 2 (extend "x" 3 empty))
            = None)
  (* etc. *)
  in b0 && b1 && b2 && b3 && b4;;

After declaring two modules that allegedly implement environments, we then invoked the unit-test function:

module Environment_association_list : ENVIRONMENT =
  struct
    ...
  end;;

let () = assert (test_environment Environment_association_list.empty
                                  Environment_association_list.extend
                                  Environment_association_list.lookup);;

module Environment_function : ENVIRONMENT =
  struct
    ...
  end;;

let () = assert (test_environment Environment_function.empty
                                  Environment_function.extend
                                  Environment_function.lookup);;

A unit-test function that expects all the components of an environment requires us to first extract these components from the corresponding module.

Unit-test functors

A more concise alternative to unit-test functions is to declare a unit-test functor that expects this module:

module Test_environment_maker (Environment : ENVIRONMENT) =
  struct
    let () = assert (Environment.lookup "x"
                                        Environment.empty
                     = None)

    let () = assert (Environment.lookup "x"
                                        (Environment.extend "x"
                                                            2
                                                            Environment.empty)
                     = Some 2)

    let () = assert (Environment.lookup "x"
                                        (Environment.extend "x"
                                                            2
                                                            (Environment.extend "x"
                                                                               3
                                                                               Environment.empty))
                    = Some 2)

    let () = assert (Environment.lookup "x"
                                        (Environment.extend "y"
                                                            2
                                                            (Environment.extend "x"
                                                                                3
                                                                                Environment.empty))
                     = Some 3)

    let () = assert (Environment.lookup "z"
                                        (Environment.extend "y"
                                                            2
                                                            (Environment.extend "x"
                                                                                3
                                                                                Environment.empty))
                     = None)
  end;;

Then, instead of invoking the unit-test function, we invoke the unit-test functor:

module Vacuous = Test_environment_maker (Environment_alist);;

module Vacuous = Test_environment_maker (Environment_fun);;

Making do without the dot notation

A programming language is low level
when its programs require attention to the irrelevant.

Alan Perlis‘s programming epigram #8

While clear in its principle, the dot notation can be heavy in practice. Look at the definition of Test_environment_maker, for example, with all the occurrences of Environment..

OCaml provides language support to elide all the occurrences of Environment. in the definition of Test_environment_maker, using the open declaration in the body of the functor. In effect, the declaration:

open Environment

tells the OCaml parser that each identifier in the body of the functor that occurs in the signature of Environment (i.e., ENVIRONMENT) should be considered as coming from Environment. As for all the other identifiers, they are lexically scoped as usual.

Using this open declaration, the unit-test functor is easier on the eye:

module Test_environment_maker_shorter (Environment : ENVIRONMENT) =
  struct
    open Environment

    let () = assert (lookup "x"
                            empty
                     = None)

    let () = assert (lookup "x"
                            (extend "x"
                                    2
                                    empty)
                     = Some 2)

    let () = assert (lookup "x"
                            (extend "x"
                                    2
                                    (extend "x"
                                            3
                                            empty))
                     = Some 2)

    let () = assert (lookup "x"
                            (extend "y"
                                    2
                                    (extend "x"
                                            3
                                            empty))
                     = Some 3)

    let () = assert (lookup "z"
                            (extend "y"
                                    2
                                    (extend "x"
                                            3
                                            empty))
                     = None)
  end;;

Exercise 14

In the same spirit as above,

  1. implement a unit-test functor for last-in, first-out queues, and
  2. implement a unit-test functor for first-in, first-out queues.

Solution for Exercise 14

See the resource file for the present lecture note.

Resources

Version

Created [02 Apr 2019]