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.