Friday, 11 December 2015

Using autotests to explore and improve code

There's a lot of code out there on the internet, however often it's hard to understand what it does and how to change it to meet your needs. I've found writing autotests a great way to delve into others' code and really understand it, including its bugs and wrinkles. Here's how I took this approach with a new programming challenge in Racket...


I want to write a version of the Eliza psychotherapist program to explore how intelligent computers can appear, or how much work it is to make them appear intelligent. I have in mind testing it out on the local kids' computer club... how many of the 8-10 year olds would be fooled?

I've had a play with the Emacs Doctor mode (run `ESC-x doctor` in emacs to see this) and it's certainly fun. I'm also exploring the Racket programming language (a dialect of Scheme) right now so if I can get something running in that then I can extend it and also further my knowledge in this language.

So after some Googling I find this:'s written for Guile (GNU's version of Scheme).

My first task is to get it running in Racket, it's not that hard, mostly quote escaping and a few differences when making hashes and sorting lists. You can see the code changes here:

So now it runs, cool :)

But it seems to crash a fair bit on certain inputs, and now I realise I have to figure out what the code is actually doing. Hmmm... I can see that `eliza.rkt` defines the patterns to match and `bot.rkt` does the actual work, but I'll need to dig much deeper to get the program working properly, and be able to extend it.

I start by listing the bugs I find, but this doesn't help much with my understand of the code...

Introducing autotests

Racket has a nice autotest framework `rackunit` so I give that a go. I pick out a few simple looking procedures from `bot.rkt` and write tests to see if I can prove what they do with different inputs. Here's my first version of `bot-tests.rkt`:

#lang racket

(require rackunit "bot.rkt")

(define-keyword (xnone)
   (A sentence for xnone)))

(define-keyword (sorry)
   (Please don\'t apologise.)))

 "pre-process-msg tests"
 (check-equal? (pre-process-msg "hello")
 (check-equal? (pre-process-msg "HeLlo")
 (check-equal? (pre-process-msg "apples AND oranges")
               '(apples and oranges))
 (check-equal? (pre-process-msg "maybe")

 "respond-to tests"
 (check-equal? (respond-to "apple and banana")
               "A sentence for xnone"))
 (check-equal? (respond-to "SORRY")
               "Please don\'t apologise."))

So I now know what `pre-process-msg` and `respond-to` do and I have a working set of tests. Now I can introduce failing tests to give me some debugging strategies...

Writing failing autotests

Here's my first failing tests, a keyword with a comma in it:

(define-keyword (you)
  ((* you *)
   (Oh, I (% 2) ?)))

(check-equal? (respond-to "you like noise")
               "Oh, I like noise ?")

So now I have a quick way of breaking the program, with a single click, rather than having to interact with the program by typing input. The other big advantage of these autotests is that they are way simpler than the full `eliza.rkt` file, so they are easier to understand. 

The fix to the above program was to escape the comma thus: `(Oh\, I (% 2) ?)`

To be continued...

Next: synonyms, what the (% 2) means in keywords, and how sentences are destructured...

Where I've got to so far

If you want to see how far I've got, take a look at my github repo:

Here's my current to-do list:

No comments:

Post a Comment