Sunday, July 26, 2009

Simple unit tests in Scheme

One nice feature of the Scheme programming is the ridiculous, over-the-top power of its "hygienic" macro system, which allows a programmer to effectively redefine the language to suit the task at hand.

A few days ago, I needed to write some simple unit tests for some Scheme code I was in the process of developing. The Guile Scheme implementation doesn't come with a library for unit testing by default, so it was necessary to create one of my own. This is it:

(define *failed-tests* '())
(define *passed-tests* '())

(define (assert-true result)
  (if (not result)
      (throw 'test-failed-exception
             (with-output-to-string
              (lambda ()
                (display "  assert-true: ")
                (display "got: ")
                (write result))))))

(define (%begin-test name test-thunk)
  (let ((test-success #t)
        (test-fail-msg #f))
    (display name) (display "... ")

    (catch #t test-thunk
           (lambda (key . args)
             (set! test-success #f)
             (set! test-fail-msg
             (if (eqv? key 'test-failed-exception)
                 (car args)
                 (with-output-to-string
                  (lambda ()
                    (display "  unexpected exception: ")
                    (write (cons key args))))))))

    (if test-success
        (begin
          (display "passed")
          (set! *passed-tests* (cons name *passed-tests*)))
        (begin
          (display "failed")
          (if test-fail-msg
              (begin
                (newline)
                (display test-fail-msg)))
          (set! *failed-tests* (cons name *failed-tests*))))
    (newline)))

(define-syntax begin-test
    (syntax-rules ()
      ((_ name . test-forms)
       (%begin-test name (lambda () . test-forms)))))

(define (report-tests)
  (display "Test summary")(newline)
  (display "Passed: ") (display (length *passed-tests*)) (newline)
  (display "Failed: ") (display (length *failed-tests*)) (newline))

Note that this effectively adds a keyword (begin-test) to the Scheme language, and this very simple macro has a massive effect on the readability of the resulting testcase code. For example, a trivial unit test using this minimalistic framework might look like:

(begin-test 'SuccessfulTest
  (assert-true #t)
  (assert-true (equal? 1 1)))
(begin-test 'FailTest
  (assert-true (equal? #t "string")))
(report-tests)

The framework is completed with the addition a few more assert- functions (left as an exercise to the reader). This code will form part of a future release of gEDA.

(In case you want to use this code in your own project, it is licensed under the GNU LGPL v3 or later). But there are probably better unit test systems for Scheme out there!

No comments: