A blog about skating and cycling, or vice versa

Testing times#

Fri, 27 Jul 2007 18:33:46 +0000

For the last, oh, seven years, I've been aware that the mature and responsible programmers all do Test-driven development and never write any code before they've written the test that justifies the need for it. Which, I guess, is fair enough, but then they go on to claim this is really enjoyable and makes programming so much more fun and at that point I just have to look at them and go "huh?". I'm happy to believe that it takes all sorts to make a world, but if I derived significant pleasure from jumping through hoops like that I'd get a job as, uh, a hoopjumper or something. If the code has particularly complex side-effects or dependence on state, TDD requires lots of fannying around with MockObjects and the like (which end up being complex enough to need tests of their own) to fake the state; if the code is written in a functional style and does the same thing every time you run it, you can test it in the repl and don't need all the framework crud.

(I think my viewpoint is probably skewed by very rarely having an external customer or sufficiently concrete requirements. This makes the programming something of an exploratory process anyway)

One place, though, that I am reluctantly forced to concede the value of testing is for avoiding regressions. But I'd still like to avoid all that effort collecting test cases and packaging them up, especially when I've probably white-box tested most of the logic in the course of developing it. So, without further ado: requires Emacs, Slime, etc -

(defslime-repl-shortcut slime-repl-shortcut-record-test ("record-test" "t")
  (:handler 'slime-dan-record-test)
  (:one-liner "Resend last input, recording it and results in \"tests.lisp\""))

(defun slime-dan-record-test () (interactive) (let ((last (copy-sequence (car slime-repl-input-history))) (package (slime-lisp-package))) (with-current-buffer "tests.lisp" (set-text-properties 0 (length last) nil last) (slime-repl-set-package package) (goto-char (point-max)) (insert "\n;; sent\n" last "\n;; received\n") (goto-char (point-max)) (slime-eval-print last))))

How this works: once you have typed in a form at the slime repl and got the right answer back, you hit , t (comma, t) and it logs the input text and return value in a file called tests.lisp (it actually evaluates the form again, but that's ok, right? it's all functional). Then you can trivially write something to repeat the tests which zips through said file evaluating odd forms in CL and comparing the results to what you get evaluating even ones. When I say "trivially", what I mean is of course "I haven't done that bit yet".

Most of th development time was spent muttering "that's not a string? that's a string? why does it look like a structure? hmm, it behaves like a string until I send it to slime" before eventually finding out that it was a string with properties. Bless you, emacs, for your backward-compatible data structures. tests.lisp ends up looking something like this -

;; sent
(+ 1 2) ; this is a comment
;; received
3
;; sent
(to-sql '(project ((as (from-universal-time date) ident) name) venue))
;; received
"SELECT FROM_UNIVERSAL_TIME(DATE) AS IDENT,NAME FROM VENUE "

Coming soon: SEPTEQL (successor of SEXQL which I wrote three years ago and have used very rarely since just because the syntax is so utterly unmemorable). I just have to clean it up a little and, uh, write some tests.

(Discovering from adjacent entries in that blog that my Athlon's about three years old makes me feel much better about replacing it with something that has a working free graphics driver - that would be an Intel motherboard, then - and doesn't die randomly for no apparent reason as the Athlon has taken to doing. I thought it was newer than that)