Lists and OUnit

In today's lab we'll have some gentle practice using lists. Then we'll learn how to unit test in OCaml using OUnit, a library similar to Java's JUnit. We need lists in order to use OUnit. Finally, we'll have some more advanced practice using lists.

Practice with lists

Becoming fluent with lists is an essential part of learning functional programming. So let's do a few exercises with lists.

Exercise: list expressions [✭]

Exercise: product [✭✭]

Write a function that returns the product of all the elements in a list. The product of all the elements of an empty list is 1. Hint: recall the sum function we defined in lecture. Put your code in a file named lab03.ml. Use the toplevel to test your code.

Exercise: concat [✭✭, optional]

Write a function that concatenates all the strings in a list. The concatenation of all the strings in an empty list is the empty string "". Hint: this function is really not much different than sum or product. Put your code in a file named lab03.ml. Use the toplevel to test your code.

Unit testing

Using the toplevel to test functions will only work for very small programs. Larger programs need test suites that contain many unit tests and can be re-run every time we update our code base. A unit test is a test of one small piece of functionality in a program, such as an individual function.

We've now learned enough features of OCaml to see how to do unit testing with a library called OUnit. It is a popular testing framework (#1 in downloads on OCaml Forge) similar to JUnit in Java, HUnit in Haskell, etc. The basic workflow for using OUnit is as follows:

The OUnit documentation is available should you wish to peruse it.

The following exercise shows you how to create an OUnit test suite. There are some things in the exercise that might at first seem mysterious; they are discussed below the exercise.

Exercise: unit test sum [✭✭]

Create a file named sum.ml, and put the following code into it:

let rec sum = function
  | []    -> 0
  | x::xs -> x + sum xs

Now create a second file named sum_test.ml, and put this code into it:

open OUnit2
open Sum

let tests = "test suite for sum" >::: [
  "empty"  >:: (fun _ -> assert_equal 0 (sum []));
  "one"    >:: (fun _ -> assert_equal 1 (sum [1]));
  "onetwo" >:: (fun _ -> assert_equal 3 (sum [1; 2]));
]

let _ = run_test_tt_main tests

(If you're using Atom as an editor, you will get some errors about OUnit2 and Sum. Ignore those for the moment: your code is correct, but Atom doesn't understand it yet. We'll fix that later in the lab by creating a .merlin file.)

Finally, run these commands:

$ ocamlbuild -pkgs oUnit sum_test.byte
$ ./sum_test.byte

You will get a response something like this:

...
Ran: 3 tests in: 0.12 seconds.
OK

Let's study more carefully what we just did in that exercise. In the test file, open OUnit2 brings into scope the many definitions in OUnit2, which is version 2 of the OUnit framework. And open Sum brings into scope the definitions from sum.ml.

Then we created a list of test cases:

[
  "empty"  >:: (fun _ -> assert_equal 0 (sum []));
  "one"    >:: (fun _ -> assert_equal 1 (sum [1]));
  "onetwo" >:: (fun _ -> assert_equal 3 (sum [1; 2]));
]

Each line of code is a separate test case. A test case has a string giving it a descriptive name, and a function to run as the test case. In between the name and the function we write >::, which is a custom operator defined by the OUnit framework. Let's look at the first function from above:

fun _ -> assert_equal 0 (sum [])

Every test case function receives as input a parameter that OUnit calls a test context. Here (and in many of the test cases we write) we don't actually need to worry about the context, so we use the underscore to indicate that the function ignores its input. The function then calls assert_equal, which is a function provided by OUnit that checks to see whether its two arguments are equal. If so the test case succeeds. If not, the test case fails.

Then we created a test suite:

let tests = "test suite for sum" >::: [
  "empty"  >:: (fun _ -> assert_equal 0 (sum []));
  "one"    >:: (fun _ -> assert_equal 1 (sum [1]));
  "onetwo" >:: (fun _ -> assert_equal 3 (sum [1; 2]));
]

The >::: operator is another custom OUnit operator. It goes between the name of the test suite and the list of test cases in that suite.

Then we ran the test suite:

let _ = run_test_tt_main tests

The function run_test_tt_main is provided by OUnit. It runs a test suite and prints the results of which test cases passed vs. which failed to standard output. The use of underscore here indicates that we don't care what value the function returns; it just gets discarded.

Finally, when we compiled the test file, we linked in the OUnit package, which has slightly unusual capitalization (which some platforms care about and others are agnostic about):

$ ocamlbuild -pkgs oUnit sum_test.byte

If you get tired of typing the pkgs oUnit part of that, you can instead create a file named _tags (note the underscore) in the same directory and put the following into it:

true: package(oUnit)

Now Ocamlbuild will automatically link in OUnit everytime you compile in this directory, without you having to give the pkgs flag. The tradeoff is that you now have to pass a different flag to Ocamlbuild:

$ ocamlbuild -use-ocamlfind sum_test.byte

And you will continue having to pass that flag as long as the _tags file exists. Why is this any better? If there are many packages you want to link, with the tags file you end up having to pass only one option on the command line, instead of many.

Testing and Merlin. If you are using Merlin (e.g., your editor is Atom or Emacs), you will notice two things that aren't quite optimal at this point. First, Merlin doesn't understand the OUnit code. Second, Merlin doesn't understand the code located in sum.ml while you are editing sum_test.ml. To fix both of those problems, create a file in the same directory and name it .merlin. (Note the leading dot in that filename.) In that file put the following:

B _build
PKG oUnit

The first line tells Merlin to look for compiled code from other source files inside the _build directory, which is where Ocamlbuild places compiled code. The second line tells Merlin to look for the oUnit package. You might need to restart your editor for these changes to take effect. After that, you definitely have to compile the code with Ocamlbuild, so that the compiled code exists in the _build directory where Merlin now expects to find it.

Exercise: failed test [✭]

Modify sum.ml to introduce a bug by changing the code in it to the following:

let rec sum = function
  | []    -> 1  (* bug *)
  | x::xs -> x + sum xs

Now rebuild and re-execute sum_test.byte. All test cases should now fail. Note how the output tells you the names of the failing cases.

Exercise: bad add [✭✭]

Create a file named add.ml, and in it put the following buggy version of an addition function:

let add x y = 
  if x mod 7 = 0 then 0   (* bug *)
  else x+y

Now create a file named add_test.ml. Create and run an OUnit test suite for add in that file. Make sure to write some test cases that will pass (e.g., add 1 2) and some test cases that will fail (e.g., add 7 1).

Exercise: product test [✭✭, optional]

Unit test the function product that you wrote in an exercise above.

More list exercises

Exercise: patterns [✭✭✭]

Using pattern matching, write three functions, one for each of the following properties. Your functions should return true if the input list has the property and false otherwise.

Exercise: library [✭✭✭]

Consult the List standard library to solve these exercises:

Exercise: library test [✭✭✭, optional]

Write a couple OUnit unit tests for each of the functions you wrote in the previous exercise.

Exercise: library puzzle [✭✭✭]

Your solutions will be only one or two lines of code each.

Exercise: take drop [✭✭✭]

Exercise: take drop tail [✭✭✭✭, recommended]

Revise your solutions for take and drop to be tail recursive, if they aren't already. Test them on long lists with large values of n to see whether they run out of stack space. Here is (tail-recursive) code to produce a long list:

(* returns:  [from i j l] is the list containing the integers from
 *   [i] to [j], inclusive, followed by the list [l].
 * example:  [from 1 3 [0] = [1;2;3;0]] *)
let rec from i j l =
  if i>j then l
  else from i (j-1) (j::l)

(* returns:  [i -- j] is the list containing the integers from
 *   [i] to [j], inclusive.
 *) 
let (--) i j =
  from i j []

let longlist = 0 -- 1_000_000

It would be worthwhile to study the definition of -- to convince yourself that you understand (i) how it works and (ii) why it is tail recursive.