The goal of this lecture note is to introduce a smoother way to carry out the unit tests.
So far we have written unit-test functions as conjunctions. For example, here is the (uncorrected) unit-test function for the factorial function as presented in the chapter about Unit tests:
let test_fac candidate =
(candidate 0 = 1)
&& (candidate 1 = 1)
&& (candidate 2 = 2)
&& (candidate 3 = 6)
&& (candidate 4 = 24)
&& (candidate 5 = 120)
(* etc. *);;
This notation is inconvenient for two reasons:
For both reasons, we are better off naming the result of each test using a let-expression:
let test_fac' candidate =
let b0 = (candidate 0 = 1)
and b1 = (candidate 1 = 1)
and b2 = (candidate 2 = 2)
and b3 = (candidate 3 = 6)
and b4 = (candidate 4 = 24)
and b5 = (candidate 5 = 121)
(* etc. *)
in b0 && b1 && b2 && b3 && b4 && b5;;
This notation is more convenient for two reasons:
it benefits from emacs’s auto-indentation; and
in case applying test_fac to a candidate factorial function yields false, it can easily be edited to visualize which test failed by constructing a tuple of the resulting Booleans instead of computing their conjunction:
let test_fac'' candidate =
let b0 = (candidate 0 = 1)
and b1 = (candidate 1 = 1)
and b2 = (candidate 2 = 2)
and b3 = (candidate 3 = 6)
and b4 = (candidate 4 = 24)
and b5 = (candidate 5 = 121)
(* etc. *)
in (b0, b1, b2, b3, b4, b5);;
In the present case, a correct candidate will fail the last test, for good reason since while 121 is the square of 11, it is not the Factorial number corresponding to 5.
Here is the corrected unit-test function for the factorial function:
let test_fac' candidate =
let b0 = (candidate 0 = 1)
and b1 = (candidate 1 = 1)
and b2 = (candidate 2 = 2)
and b3 = (candidate 3 = 6)
and b4 = (candidate 4 = 24)
and b5 = (candidate 5 = 120) (* <-- correction *)
(* etc. *)
in b0 && b1 && b2 && b3 && b4 && b5;;
Likewise, here are the unit-test functions for the Fibonacci function (cf. Unit tests) and for Boolean negation (cf. Exercise 11 in Week 03):
let test_fib candidate =
let b0 = (candidate 0 = 0)
and b1 = (candidate 1 = 1)
and b2 = (candidate 2 = 1)
and b3 = (candidate 3 = 2)
and b4 = (candidate 4 = 3)
and b5 = (candidate 5 = 5)
and b6 = (candidate 6 = 8)
(* etc. *)
in b0 && b1 && b2 && b3 && b4 && b5 && b6;;
let test_not candidate =
let b0 = (candidate true = false)
and b1 = (candidate false = true)
in b0 && b1;;
OCaml provides a generator of random non-negative integers such that for any non-negative integer n, applying the generator to succ n returns a random number between 0 and n:
# (Random.int 5, Random.int 5, Random.int 5, Random.int 5);;
- : int * int * int * int = (1, 2, 0, 4)
#
Remembering the inductive definition of factorial numbers, i.e.,
we can integrate them in the unit-test function for the factorial function using random numbers:
let test_fac candidate =
(* intuitive numbers: *)
let b1 = (candidate 1 = 1)
and b2 = (candidate 2 = 2)
and b3 = (candidate 3 = 6)
and b4 = (candidate 4 = 24)
and b5 = (candidate 5 = 120)
(* base case: *)
and bc = (candidate 0 = 1)
(* instance of the induction step: *)
and is = (let k = Random.int 20
in candidate (k + 1) = (candidate k) * (k + 1))
(* etc. *)
in b1 && b2 && b3 && b4 && b5 && bc && is;;
This unit-test function now features
A few observations:
Let us revisit the function that computes a squared sum:
let square n =
n * n;;
let squared_sum i j =
square (i + j);;
We can strengthen its unit-test function by checking that the candidate function satisfies the binomial theorem at rank 2 as well as any algebraic identity we fancy:
let test_squared_sum candidate =
(* intuitive numbers: *)
let b0 = (candidate 0 0 = 0)
and b1 = (candidate 0 1 = 1)
and b2 = (candidate 0 2 = 4)
and b3 = (candidate 1 0 = 1)
and b4 = (candidate 2 0 = 4)
and b5 = (candidate 1 2 = 9)
and b6 = (candidate 2 1 = 9)
and b7 = (candidate 5 5 = 100)
(* instance of the binomial theorem at rank 2: *)
and bt = (let i = Random.int 1000
and j = Random.int 1000
in candidate i j = square i + 2 * i * j + square j)
(* twice the same integer: *)
and twice = (let i = Random.int 1000
in candidate i i = square (2 * i))
(* etc. *)
in b0 && b1 && b2 && b3 && b4 && b5 && b6 && b7 && bt && twice (* etc. *);;
But again, this unit-test function is still incomplete: it would not detect a fake function that misbehaves for integers greater than 999.
For a mouthful of other examples,
All of these properties we can check in a unit-test function parameterized with an upper bound for the random numbers:
let test_add_mul n add mul =
(* commutativity of addition: *)
let a0 = (let x = Random.int n
and y = Random.int n
in add x y = add y x)
(* associativity of addition: *)
and a1 = (let x = Random.int n
and y = Random.int n
and z = Random.int n
in add x (add y z) = add (add x y) z)
(* identity element: *)
and a2 = (let x = Random.int n
in add 0 x = x)
and a3 = (let x = Random.int n
in add x 0 = x)
(* commutativity of multiplication: *)
and m0 = (let x = Random.int n
and y = Random.int n
in mul x y = mul y x)
(* associativity of multiplication: *)
and m1 = (let x = Random.int n
and y = Random.int n
and z = Random.int n
in mul x (mul y z) = mul (mul x y) z)
(* identity element: *)
and m2 = (let x = Random.int n
in mul 1 x = x)
and m3 = (let x = Random.int n
in mul x 1 = x)
(* absorbing element: *)
and m4 = (let x = Random.int n
in mul x 0 = 0)
and m5 = (let x = Random.int n
in mul 0 x = 0)
(* distributivity: *)
and d0 = (let x = Random.int n
and y = Random.int n
and z = Random.int n
in mul (add x y) z = add (mul x z) (mul y z))
and d1 = (let x = Random.int n
and y = Random.int n
and z = Random.int n
in mul x (add y z) = add (mul x y) (mul x z))
in a0 && a1 && a2 && a3 && m0 && m1 && m2 && m3 && m4 && m5 && d0 && d1;;
For the sake of sanity, let us test this unit-test function with reference implementations of addition and multiplication:
# test_add_mul 100 ( + ) ( * );;
- : bool = true
# test_add_mul 1000 ( + ) ( * );;
- : bool = true
# test_add_mul 10000 ( + ) ( * );;
- : bool = true
#
Alfrothul: Watch these spaces.Harald: No comments.
Implement a gorgeous unit-test function for string concatenation (i.e., (^) in OCaml).
Harald: That’s at least the third time we come across these concepts.Loki (in a funereal tone): Déjà vu. Again.
Food for thought:
The following two OCaml functions could come handy:
a generator of random characters:
let random_char () =
char_of_int (Random.int 32 + 32);;
the library function String.make that has type int -> char -> string and, when given a non-negative integer n and a character c, returns a string of length n containing c, n times:
# String.make 2 'a';;
- : string = "aa"
#
Harald: Could come handy?
Alfrothul: Maybe to generate a random string?
Harald: Let me try:
# String.make 2 (random_char ());;
- : string = "11"
# String.make 2 (random_char ());;
- : string = "%%"
# String.make 2 (random_char ());;
- : string = "--"
# String.make 2 (random_char ());;
- : string = "66"
#
Alfrothul: That seems to be the case. And if we modify the first argument, we should get a random string of another length:
# String.make 3 (random_char ());;
- : string = "???"
#
Harald: Huh?
Alfrothul (smiling): You mean “Huh?”? It’s just a string with 3 times the character '?'.
Harald: Harumph. But I think I see what you mean:
# String.make 4 (random_char ());;
- : string = "<<<<"
# String.make 5 (random_char ());;
- : string = "!!!!!"
# String.make 6 (random_char ());;
- : string = "......"
#
Alfrothul: OK. So we can manufacture a string of a given length containing repeated occurrences of the same random character. We could even implement a function to do hat:
let random_string n =
String.make n (random_char ());;
Harald: Well, OCaml says it has type int -> string.
Alfrothul: And it works too:
# random_string 2;;
- : string = "44"
# random_string 3;;
- : string = "%%%"
# random_string 4;;
- : string = "9999"
#
Harald: What if we apply random_string to 0?
Brynja: It’s not going to be very random, is it.
Alfrothul: Well, there is only one string of length 0, and it is the empty string, so, yes, not random at all:
# random_string 0;;
- : string = ""
# random_string 0;;
- : string = ""
# random_string 0;;
- : string = ""
#
Harald: On the other hand, we could generate strings of random length:
# random_string (Random.int 10);;
- : string = " "
# random_string (Random.int 10);;
- : string = "######"
# random_string (Random.int 10);;
- : string = ""
# random_string (Random.int 10);;
- : string = "<<<<<<<<<"
#
Alfrothul: We might as well bake that in the definition of random_string:
let random_string n =
String.make (Random.int n) (random_char ());;
Harald: That should work:
# random_string 20;;
- : string = "0000000000000000000"
# random_string 20;;
- : string = "//////////"
# random_string 20;;
- : string = "444"
# random_string 20;;
- : string = "444444"
#
Alfrothul: OK. So we now have a function that, given a positive integer, returns a string of random length containing repeated occurrences of the same random character.
Loki (ever the helpful one): And now you are equipped to test whether string concatenation is commutative, for example.
OCaml features assert-expressions:
If e evaluates to true, then assert e evaluates to (), and if e evaluates to false, then OCaml complains:
# assert true;;
- : unit = ()
# assert false;;
Exception: Assert_failure ("//toplevel//", 1573, -52).
#
We can thus treat our unit tests as assertions: if a function passes its unit tests, we don’t want to hear about it, whereas if it doesn’t, we very much do. For example:
# let () = assert (test_successor (fun n -> n + 1) = true);;
# let () = assert (test_successor (fun n -> n) = true);;
Exception: Assert_failure ("//toplevel//", 1573, -43).
#
The second assertion fails because the identity function does not pass the unit tests for the successor function.
More concisely, we can write:
# let () = assert (test_successor (fun n -> n + 1));;
# let () = assert (test_successor (fun n -> n));;
Exception: Assert_failure ("//toplevel//", 1573, -43).
#
So from now on, instead of writing comments in our .ml files to the effect that a function passes its unit test, we will simply assert that they do:
let () = assert (test_successor (fun n -> n + 1));;
And when the file is loaded, we will be alerted if a function does not pass its unit tests.
In other words, from now on, what goes well goes without saying.
As a concrete example, download the resource file, edit it using Emacs in Tuareg mode, load it in OCaml (by typing #use "week-04_unit-tests.ml"), and observe how:
Namely:
# #use "week-04_unit-tests.ml";;
val test_successor : (int -> int) -> bool = <fun>
val successor_v0 : int -> int = <fun>
val successor_v0' : int -> int = <fun>
val successor_v1 : int -> int = <fun>
val successor_v1' : int -> int = <fun>
val identity : 'a -> 'a = <fun>
Exception: Assert_failure ("week-04_unit-tests.ml", 45, 9).
#
The error message tells us that at Line 45, the assertion is not satisfied. (To be precise, the expression assert (test_successor identity);; starts at Column 9 of Line 45.)
Sometimes, it is useful to verify that a test fails. In the present case, since we expect that identity will not pass the unit test for the successor function, we can state this as such in the file, and write:
let () = assert (test_successor identity = false);;
instead of:
let () = assert (test_successor identity);;
The resulting modified file loads without any hiccup:
# #use "week-04_unit-tests-modified.ml";;
val test_successor : (int -> int) -> bool = <fun>
val successor_v0 : int -> int = <fun>
val successor_v0' : int -> int = <fun>
val successor_v1 : int -> int = <fun>
val successor_v1' : int -> int = <fun>
val identity : 'a -> 'a = <fun>
- : string = "week-04_unit-tests-modified.ml"
#
The recommended way to compose one’s .ml file is as follows:
This way, every time the file is loaded, the unit tests will be run.
This exercise assumes that the reader is convinced that unit tests are A Good Thing, and that it is Very Useful to compose one’s .ml file as recommended above, so that (1) the unit-test functions are written before the functions to test are implemented, and (2) the implemented functions are automatically tested when the file is loaded, which guarantees that the latest version of the implementation is tested.
Under this assumption, and besides the fact that the identity function fails the unit test for the successor function, there is an all-too-common error both in the resource file and in the modified resource file. What is this embarrassing error?
Assertions make it possible to verify in passing that conditions are satisfied. For example, the implementation of test_add_mul expects its first argument to be a positive integer. That it is an integer is a typing property, and this property is therefore ensured by OCaml’s type system. But there is no type for positive integers, and therefore this implementation is vulnerable to non-positive integers:
# test_add_mul 0 ( + ) ( * );;
Exception: Invalid_argument "Random.int".
# test_add_mul ~-1 ( + ) ( * );;
Exception: Invalid_argument "Random.int".
#
The error message reports how things went wrong in the implementation of test_add_mul, which is uninformative to the user. It would be more informative to report what went wrong at the point where it went wrong. To this end, let us insert an assertion at the outset of the implementation:
let test_add_mul n add mul =
let () = assert (n > 0) in
(* commutativity of addition: *)
let a0 = ...
This unit-test function was vulnerable to being applied to a non-positive integer. It is now shielded.
Created [06 Feb 2020]