signature ASSERT = sig
type testresult = (string * bool);
type tcase;
type raisesTestExn;
val It : string -> (unit -> raisesTestExn) -> tcase;
val T : (unit -> raisesTestExn) -> tcase;
val Pending : string -> (unit -> raisesTestExn) -> tcase;
val succeed : string -> raisesTestExn;
val fail : string -> raisesTestExn;
val == : (''a * ''a) -> raisesTestExn;
val =/= : (''a * ''a) -> raisesTestExn;
val != : (exn * (unit -> 'z)) -> raisesTestExn;
val =?= : (''a * ''a) -> ''a;
val runTest : tcase -> testresult;
val runTests : tcase list -> unit;
end
1: Include the file assert.sml in your project.
use "assert";
open Assert;
infixr 2 == != =/= =?=;
val myTests = [
It "adds integers" (fn() => 2 + 2 == 5),
It "concatenates strings" (fn() => "foo" ^ "bar" == "foolbar"),
It "raises Subscript" (fn()=> Subscript != (fn() => String.sub("hello", 1)))
];
> runTests myTests;
FAILED adds integers
4 <> 5
FAILED concatenates strings
"foobar" <> "foolbar"
FAILED raises Subscript
Subscript <> ~ran successfully~
TESTS FAILED: 3/3
$ echo $?
1
Create tests with either the T
function
T (fn () => 2 + 2 == 4)
Or the It
function:
It "can put two and two together" (fn () => 2 + 2 == 4)
Or, if you want to exclude the test from execution:
Pending "this is for later" (fn () => a() == b())
You can run an individual test with runTest
:
> val t1 = T (fn () => 2 + 2 == 4);
val t1 = TC ("", fn): tcase
> runTest t1;
val it = ("OK \n\t4 = 4\n", true): testresult
The testresult
type is a tuple where the first element is a printable
description of the test result, and the second element indicates success or
failure.
> val t2 = T (fn () => "a" ^ "b" == "abc");
val t2 = TC ("", fn): tcase
> runTest t2;
val it = ("FAILED \n\t\"ab\" <> \"abc\"\n", false): testresult
You can imperatively run a testcase list
to get formatted output printed to
stdout, and have the entire SML program exit with a POSIX success code if there
were no failures, and an error code if some tests did not pass.
runTests [t1, t2];
FAILED
"ab" <> "abc"
TESTS FAILED: 1/2
$ echo $?
1
Let's take a look at the type of the It
function above:
> Assert.It;
val it = fn: string -> (unit -> Assert.raisesTestExn) -> Assert.tcase
It takes a string that describes the test case, and then a function typed
(unit -> Assert.raisesTestExn)
. How do we obtain such a function? By
embedding within its body one of the assertions offered by the module. They are
listed below.
This assertion 'manually' passes a test. For example, in cases where the data under test doesn't support equality.
> val t1 = T (fn () => if Real.==(Real.*(2.0, 2.0), 4.0)
then succeed "reals are equal"
else fail "reals not equal");
val t1 = TC ("", fn): tcase
> runTest t1;
val it = ("OK \n\treals are equal = reals are equal\n", true): testresult
The counterpart to succeed
. Makes a test fail when executed.
> val t2 = T (fn () => if Real.==(Real.*(2.0, 2.0), 5.0)
then succeed "reals are equal"
else fail "reals not equal");
val t2 = TC ("", fn): tcase
> runTest t2;
val it = ("FAILED \n\treals not equal <> ~explicit fail~\n", false):
testresult
Fails the test case if left
and right
are not equal. The first element of
the testresult will contain string representations of the data (courtesy of
PolyML.makestring
).
> val t4 = T (fn () => {a="record"} == {a="cd"});
val t4 = TC ("", fn): tcase
> runTest t4;
val it = ("FAILED \n\t{a = \"record\"} <> {a = \"cd\"}\n", false): testresult
> print (#1 it);
FAILED
{a = "record"} <> {a = "cd"}
val it = (): unit
The inverse of ==
. Will fail the test case if left
and right
are equal.
Succeeds when f
, after evaluation, raises exception exn
. Both the exception
name and message must match. If the function runs successfully, the test case
is counted as a failure.
> runTest (T (fn () => (Boom "Aaa!") != (fn () => raise Boom "zzz")));
val it = ("FAILED \n\tBoom \"Aaa!\" <> Boom \"zzz\"\n", false): testresult
> print (#1 it);
FAILED
Boom "Aaa!" <> Boom "zzz"
val it = (): unit
> runTest (T (fn () => (Boom "Aaa!") != (fn ()=> 2 + 2)));
val it = ("FAILED \n\tBoom \"Aaa!\" <> ~ran successfully~\n", false):
testresult
> print (#1 it);
FAILED
Boom "Aaa!" <> ~ran successfully~
val it = (): unit
> runTest (T (fn () => (Boom "Aaa!") != (fn () => raise Boom "Aaa!")));
val it = ("OK \n\tBoom \"Aaa!\" = Boom \"Aaa!\"\n", true): testresult
> print (#1 it);
OK
Boom "Aaa!" = Boom "Aaa!"
val it = (): unit
This is a classic "assert" function, in the sense that it will simply return
left
if it's equal to right
, but if the two operands are not equal, it
will fail the entire test case.
Useful for getting around match exhaustiveness warnings when you want match-based assertions throughout your test, like in Erlang. This approach is problematic in Standard ML, because "assertively" matching on expected values will generate "Matches are not exhaustive" messages, like below:
let val ALLGOOD = someOp();
val foo = worksOnAllGood(ALLGOOD);
...
If we'd like to get rid of all exhaustiveness warnings, we can use =?=
to
encode our expectations on the right side of the match, while keeping the left
side non-specific, like so:
let val ag = (someOp() =?= ALLGOOD);
val foo = worksOnAllGood(ag);
...
The above will fail the test if someOp
does not return ALLGOOD. If it does,
it'll bind ag
to ALLGOOD
and proceed to evaluate subsequent expressions as
normal.