Simple unit testing with Chicken Scheme
I’m currently translating a small toy project of mine to Chicken Scheme. Since the original (written in Python) has unit tests, it made sense to look for similar functionality for Chicken. As it turns out, there are several testing frameworks available. Since right now I’m still exploring, I settled for a relatively simple one, the test egg (by Alex Shinn).
Unlike Python’s unittest, the test library is not object-oriented at all. Rather, it defines a small number of useful functions and macros, which can be used at will (e.g. nested, inside lets and loops, etc).
The most important interface is (test <expected-value> <expression>), which, naturally, checks if expression evaluates to expected-value. For example, (test 4 (+ 2 2)) would be a valid use of test. (Note that switching the arguments does *not* work; (test (+ 2 2) 4) is illegal.)
Here’s some actual code:
(use test) (load "tools") (test-group "make-address" (test #xC000 (make-address #xC0 #x00)) (test #x1234 (make-address #x12 #x34))) (test-group "high" (test #x08 (high #x0801)) (test #xC0 (high #xC000)) (test #x12 (high #x1234))) (test-group "low" (test #x01 (low #x0801)) (test #x00 (low #xC000)) (test #x34 (low #x1234))) (test-group "signed->unsigned" (define s->u signed->unsigned) (test 0 (s->u 0)) (test 20 (s->u 20)) (test #xFF (s->u -1)) (test #xF7 (s->u -9)) (test #x80 (s->u -128)) (test-error (s->u 200))) (test-group "unsigned->signed" (define u->s unsigned->signed) (test 0 (u->s 0)) (test 20 (u->s 20)) (test -128 (u->s #x80)) (test -1 (u->s #xFF)) (test -9 (u->s #xF7)) (test-error (u->s -1)))
The test-group form is a simple but effective way to group tests together; they show up in separate sections in test reports. It has its own lexical scope, so anything defined inside it is not visible outside of it.
When an expression is expected to produce an error, use test-error to catch this and test for it. To simply assert that an expression is non-false, use test-assert.
Of course, tests can be used inside loops, which makes it really easy to test each item in a list. Each of these tests will show up as a separate entry in the resulting test report. (Something which isn’t so easy to accomplish with Python’s unittest.) Some more actual code (which tests all items in a list, and defines a meaningful name for each test):
(test-group "*opcodes*"
(define (test-opcode opcode)
(define test-name (sprintf "test opcode: ~s" opcode))
(test-assert test-name (member (opcode-type opcode) *opcode-types*)))
(for-each test-opcode *opcodes*))
When running these scripts, test reports are produced that look like this:
-- testing make-address ------------------------------------------------------ (make-address 192 0) ................................................. [ PASS] (make-address 18 52) ................................................. [ PASS] 2 tests completed in 0.002 seconds. 2 out of 2 (100%) tests passed. -- done testing make-address ------------------------------------------------- -- testing high -------------------------------------------------------------- (high 2049) .......................................................... [ PASS] (high 49152) ......................................................... [ PASS] (high 4660) .......................................................... [ PASS] 3 tests completed in 0 seconds. 3 out of 3 (100%) tests passed. -- done testing high ---------------------------------------------------------
…etc.
The test egg works for my current purposes; writing tests is easy, and so is grouping them, or running several files of tests. (Just create a file test-all.scm that loads all the other test files.) However, in the future I might want to look for a testing framework that is a little more configurable. For example, as far as I can tell, it’s impossible to make small changes to the test report output, without rewriting the whole function that handles it (and registering it as the current-test-group-reporter). But I’ll stick with it for now — it’s a great way to do real testing (with surprising flexibility) with little work.
Update (2008-01-24): I found it somewhat annoying that, if you have a lot of tests in separate groups, you have to scroll up in the terminal window to see if any failed, because totals are only displayed for each group, but not for all groups as a whole. However, this is easily fixed with a small shell script:
csi -s test-all.scm | grep FAIL
shows quickly if there were any failures. It’s not perfect (e.g. it doesn’t show which groups the failed tests were in), but it helps.
Kon Lovett said,
January 22, 2008 @ 4:28 pm
The “testbase” suite is a possibility. However I would never claim it to be “easy”. Alex’s “test” and Neil’s “testeez” are much more approachable. Another pkg you might run into is “SchemeUnit”. While not available for Chicken (“testbase” has similar facilities) you will come across it with PLT Scheme.
I wrote “testbase” & use it for my eggs so there are a number of real-world examples. I don’t have a configurable report generator, however there are a number of pre-defined available. Also “testbase”, used with the testbase driver, will build a database of results.
Flavor example:
(use testbase testbase-output-compact)
(define-test opcode-test “Opcodes”
(initial
(define (test-opcode opcode)
(define test-name (sprintf “test opcode: ~s” opcode))
(expect-success test-name (member (opcode-type opcode) *opcode-types*))) )
(test/collect “All opcodes”
(for-each (lambda (opcode) (collect-test (test-opcode opcode))) *opcodes*) ) )
(test::output-style-compact (opcode-test))
SUITE Passed: Opcodes
CASE Passed: All opcodes
EXPECTATION: test opcode: #
Expect success
Passed: test opcode: #
EXPECTATION: test opcode: #
Expect success
Passed: test opcode: #
EXPECTATION: test opcode: #
Expect success
Passed: test opcode: #
ALL TESTS SUCCESSFUL!
#t
Ian Bicking said,
January 28, 2008 @ 11:25 pm
I harp on this frequently, but: write doctest for Scheme! It even has docstrings (though stand-alone files are also nice), repr, and a super-easy-to-use eval. And its tokenizer is also easy to use. It should be much easier to write than in Python.
Hans Nowak said,
January 29, 2008 @ 10:31 am
@Ian: Maybe I will. :) It should be an interesting project to do, and maybe to contribute (as an egg or otherwise). I’m not sure I know enough about all the details yet, though, like the docstrings.