'live' testing style

spir denis.spir at gmail.com
Sun Feb 13 14:03:06 PST 2011


Hello,


Back to the subject of using unittest and assert. I'll try to illustrate a 
testing style that seems to be rare in the D community, is not properly 
supported by D builtin tools --while this would require only minimal 
improvements--, and is imo rather sensible, practicle and efficient. Using an 
example of a current project of mine (a toy homoiconic language, kind of 
special Lisp).

Below is a copy of part of the parser's test suite. Maybe have a quick look 
before reading further [Note: I don't mean the example is a model of any sort, 
not even it's good code. It is just illustration of some testing practice.] The 
testList func tests the match func for List literal notations. Some comments, 
in form of question-answer:

* Why isn't testList a unittest block?

Using named funcs, I can switch on & off specific test suites by (un)commenting 
their call from the main and unique unittest block. Else, either they all run, 
or none. During development, I only keep active the test func(s) relative to 
the feature I'm currently working on.
Remedy: named unittests.

* Why those write(f)(ln) in unitests?

For me unittests are not only regression tests, to be used /after/ development 
for maintenance purpose. Instead, they are the #1 and necessary source of 
information about what the code actually does, how, and why. I constantly run 
tests, for feedback, just to check all runs fine, or diagnose what goes wrong.
Thus, the first method I write for a type if often toString; I give it a form 
which tells me all I need about an object; commonly, it's just the notation to 
re-create it. This, and having test in mind for writing toString, also helps in 
properly defining the type itself.
For diagnosis, this information is of primordial value. And, against common 
sense maybe, data about what goes right, or seems to, is commonly even more 
important than data about what goes wrong, to determine the cause(s). (*)
Remedy: verbose asserts on request.

* What are those '// for diagnose' statement?

Since the test func has a form of loop over a test suite, a failing assert 
would fail (!) to tell which case is in cause, instead miserably spitting out a 
line number only. To get the proper info, I just need to comment out those few 
lines of code, which would output for instance (artificial failure case):
...
`[nil false  true]` --> [nil false true]
`["a"  "ab"   "abc"]` --> ["a" "ab" "abc"]
`[[3] [3 2] [3 1 2]]` --> [[3] [3 2] [3 1 2]]
*** error *** expected: [[3] [32] [3 1 2]]
Remedy: improved assertion failure message form.

* Why string'ed results?

In this case, like in many others due to various reasons, it's uneasy to write 
correct result objects; instead I would spend much stupid time in trying to 
construct by hand portions of ASTs. But I have just what I need already: normal 
runs of unitests in verbose mode write them out for me. Once I'm convinced, as 
much as possible, all works fine, I just copy paste result /strings/ into 
assert statements.
Then only I activate said assertions and switch verbose mode off, thus getting 
a regression test suite for free. Even better, this test suite holds 
information providing code, ready to use in case of failure.
Remedy: assert converts test result to string if needed.



Summary:

1. Named unittests allowing test suites in the form of (just an example):

unittest test1 {
     ...
}
unittest test2 {
     ...
}
unittest test3 {
     ...
}
unittest {
     test1;
     test2;
     test3;
}

/Unnamed/ unittests are run with --unittest. Named ones are intended to be 
called from unnamed ones. Backward compatible change.


2. A variant of assert(), or better a distinct check() statement, in the form of:

     check(expression, expectation)

Separating arguments allows writing out messages like (example formats):

* case success (outcome == expectation):
     <expression> --> <outcome>
bringing valuable feedback during development.

* case failure
     *** test failure **********
     expression  : <expression>
     outcome     : <outcome>
     expectation : <expectation>
     ***************************
In silent mode, check() writes only in case of failure.

If the expected result is a separated argument, it is trivial for check to call 
to!string on the outcome when it's not a string and the expected result is a 
string; then only compare.

I guess both changes require compiler support (but may very well be wrong).
As illustrated by the example, D does not prevent one to use such testing 
practices; instead, its very nice string and literal features make it far 
better suited for that than any other static language I know.
Still, like Walter, I think it's important for (good) testing practices to be 
supported by the language, so-to-say officially. 'unittest' itself does not 
bring any feature: one could trivially have a 'test' control func intended to 
be (un)commented. But the fact it's a builtin feature makes a decisive 
difference; if only because it then becomes part of the D community's shared 
culture.


Denis


(*) Please don't argue on this point, I have been a maintenance engineer for 
five years, diagnosing broken machines everyday.


=== sample part of test code ====================
void testList () {
     writeln("=== List ===============================================");
     List list;
     LexemeStream lexemes;

     // test data
     string[] sources = [
         `[  ]` ,
         `[ 3    ]` ,
         `[ 3   1  2 ]` ,
         `[zxy  x   zx]` ,
         `[nil false  true]` ,
         `["a"  "ab"   "abc"]` ,
         `[[3] [3 2] [3 1 2]]` ,
         `[3 "abc" zxy nil [3 1 2]]` ,
     ];
     string[] correctResultStrings = [
         `[]` ,
         `[3]` ,
         `[3 1 2]` ,
         `[zxy x zx]` ,
         `[nil false true]` ,
         `["a" "ab" "abc"]` ,
         `[[3] [3 2] [3 1 2]]` ,
         `[3 "abc" zxy nil [3 1 2]]` ,
     ];

     // test run
     foreach (i,source ; sources) {
         lexemes = claroLexer.lexemes(source);
         list = cast(List)matchList(lexemes);
         // output
         writefln("`%s` --> %s", source,list);
         // for diagnose
         if ( to!string(list) != correctResultStrings[i] ) {
             writefln("*** error *** expected: %s", correctResultStrings[i]);
             stop(); // core.stdc.stdlib.exit(0)
         }
         // regression test assertion
         assert ( to!string(list) == correctResultStrings[i] );
     }
     writeln();
}
...
unittest {
     testList();
//~     testUnit();
//~     testStatement();
//~     ...
}
=================================================
=== success output in 'verbose' mode ============
`[  ]` --> []
`[ 3    ]` --> [3]
`[ 3   1  2 ]` --> [3 1 2]
`[zxy  x   zx]` --> [zxy x zx]
`[nil false  true]` --> [nil false true]
`["a"  "ab"   "abc"]` --> ["a" "ab" "abc"]
`[[3] [3 2] [3 1 2]]` --> [[3] [3 2] [3 1 2]]
`[3 "abc" zxy nil [3 1 2]]` --> [3 "abc" zxy nil [3 1 2]]
=================================================

-- 
_________________
vita es estrany
spir.wikidot.com



More information about the Digitalmars-d mailing list