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.