NYCJUG/TestCases
Some examples of creating test cases to go along with code. Some of these examples were driven by algorithm development detailed here.
Sample #1
It's good practice to create a test case at the same time as a piece of code because this provides immediate and re-producible validation of that code. For instance, the following code purports to return a one for a numeric argument and zero for non-numeric.
NB.* isNum: 1 iff y is numeric, 0 if not isNum=: 1 4 8 16 64 128 1024 4096 8192 16384 e.~ 3!:0
However, as written, this code is opaque since it relies on internal type numbers. The following test case gives us greater confidence in the validity of this code by including both numeric and non-numeric arguments. In general, we want to test both "success" and "failure" cases.
isNumTC0_testCases_=: 3 : 0 assert. 1 *./ . = isNum_base_ &> 1;(i.3);2j1;3.14;99x;i.2 4 assert. 0 *./ . = isNum_base_ &> '3';(<<99);'hi';2 3$'0' )
Here, we link the test case to the object of testing by embedding the name "isNum" in the test case name. We also segregate the test case into its own namespace though this requires us to assume the verb to be tested resides in the "base" namespace.
Alternative Approach
Test-driven development (TDD) is a useful evolution in programming practice and is seeing increased uptake worldwide. That said, TDD does not obviate the need for transparent code that is dependable on its face. To paraphase WikiPedia:C. A. R. Hoare in his Turing Award lecture:
There are two ways to write a program: One way is to make it
so simple that there are obviously no bugs, and the other way
is to make it so complicated that there are no obvious bugs.
As an example, it might be useful to express the isNum test in a more platform-neutral way, independent of the internal type numbers the existing J implementation happens to use. To wit: isNum =: 0 -: [: {. 0 $ ,@:] NB. Or use 0 {.@:($,) ] formulation to take advantage of special code
This code works because the J dictionary makes two promises:
1. The fill (and therefore, overtake) for numeric arrays is always zero 1. All numbers are analytic (meaning: irrespective of internal representation, a zero is a zero is a zero)
If one worries about the dependability of empty arrays, one could always express this as 0 -: [: {: (1 + */@:$) {. ,@] . In any case, another use of the test cases defined above is to give us confidence our new formulation is identical in practice to the original (and running them with the new definition demonstrates this).
Refinements to Test Cases
We can remove the namespace dependency by changing our test case into an adverb which takes the verb to be tested as its argument. Also, we may want to return a more positive result than silence for success.
isNumTC0_testCases_=: 1 : 0 assert. 1 *./ . = u &> 1;(i.3);2j1;3.14;99x;i.2 4 assert. 0 *./ . = u &> '3';(<<99);'hi';2 3$'0' 1 )
We invoke the test cases, with either definition of isNum shown above, like this:
isNum_base_ isNumTC0_testCases_ 1
We still have to qualify isNum by its namespace but this way makes it apparent by not hiding it inside the test case. Also, we might want to have multiple versions of the target verb available while we're developing it, perhaps like this:
isNum_older_=: 1 4 8 16 64 128 1024 4096 8192 16384 e.~ 3!:0 isNum_newer_=: 0 -: [: {. 0 $ ,@:] NB. Or use 0 {.@:($,) ] formulation to take advantage of special code
So now it's patently obvious which we're testing:
isNum_newer_ isNumTC0_testCases_ 1 isNum_older_ isNumTC0_testCases_ 1
Using an adverb for the test cases de-couples the test from the item being tested, allowing us to easily add more testing as we think of it.
isNumTC0_testCases_=: 1 : 0 assert. 1 *./ . = u &> 1;(i.3);2j1;3.14;99x;3r2;i. 2 4 assert. 0 *./ . = u &> '3';(<<99);'hi';2 3$'0' abc=. i.100 [ fpHilbert=. %>:i.9 9 [ ratHilbert=. %>: x: i.9 9 assert. 1 *./ . = u &> abc;fpHilbert;ratHilbert xyz=. '99' assert. 0 *./ . = u &> (<xyz),":&.>abc;fpHilbert;ratHilbert 1 ) isNum_older_ isNumTC0_testCases_ 1 isNum_newer_ isNumTC0_testCases_ 1