UP | HOME

Marcus Santos

Emacs & Lisp enthusiast

A simple Unit Test framework in Lisp
Published on Jan 11, 2021 by Marcus Santos.

Chapter 9 of Peter Siegel’s book Practical Common Lisp presents an interesting application for Lisp macros: a language for defining simple unit tests.

This article provides a use case for that unit test language. For more details about macros, please read the aforementioned chapter of Peter Siegel’s book.

We assume you are using either emacs+slime or common-lisp-jupyter to load and run your programs.

Creating and running test cases: steps

Step 1: defining the unit test language

  • Emacs+slime users: C-x C-f to create a new buffer called unit-test.lisp, copy paste the program below in a buffer, C-x C-s to save it, then load the program by executing the command below on the REPL:

    USER-RTL> (load "unit-test.lisp")
    
  • common-lisp-jupyter users: copy paste the program below in a common-lisp code cell, then load it by clicking on Run.
;; Macros

(defvar *test-name* nil)

(defun report-result (result form)
  (format t "~:[FAIL~;pass~] ...~a: ~a~%" result *test-name* form)
  result)

(defmacro with-gensyms ((&rest names) &body body)
  `(let ,(loop for n in names collect `(,n (gensym)))
     ,@body))

(defmacro combine-results (&body forms)
  (with-gensyms (result)
    `(let ((,result t))
       ,@(loop for f in forms collect `(unless ,f (setf ,result nil)))
       ,result)))

(defmacro check (&body forms)
  `(combine-results
     ,@(loop for f in forms collect `(report-result ,f ',f))))

(defmacro deftest (name parameters &body body)
  `(defun ,name ,parameters
     (let ((*test-name* (append *test-name* (list ',name))))
       ,@body)))

Step 2: creating your lisp program

Suppose we would like to test the lisp program below consisting of the definition of function COUNT-VOWELS.

  • Emacs+slime users: C-x C-f to create a new buffer called my-program.lisp, copy paste the program below in a buffer, =C-x C-s“ to save it, then load the program by executing the command below on the REPL:

    USER-RTL> (load "my-program.lisp")
    
  • common-lisp-jupyter users: copy paste the program below in a new common-lisp code cell, then load it by clicking on Run.
(defun count-vowels (str)
  (do ((i 0 (1+ i))
       (acc 0)
       (len (length str)))
      ((= i len) acc)
    (when (or (equal (aref str i) #\a) (equal (aref str i) #\e)
              (equal (aref str i) #\i) (equal (aref str i) #\o)
              (equal (aref str i) #\u))
      (:= acc (1+ acc)))))

Step 3: creating the unit tests

Now it is time to define the unit tests for the functions of your program.

  • Emacs+slime users: C-x C-f to create a new buffer called test.lisp, copy paste the program below in a buffer, =C-x C-s“ to save it, then load the program by executing the command below on the REPL:

    USER-RTL> (load "test.lisp")
    
  • common-lisp-jupyter users: copy paste the program below in a new common-lisp code cell, then load it by clicking on Run.
(deftest test-count-vowels ()              
  (check                     
    (= (count-vowels "") 0)  ; Assertions
    (= (count-vowels "b") 0)
    (= (count-vowels "Assdva") 1)))

(defun main ()                              
  (test-count-vowels))

Step4: running your tests

To run your tests,

  • emacs+slime users type (MAIN) in the REPL
  • common-lisp jupyter users type (MAIN) in a new code cell and click on Run
RTL-USER> (main)
pass ...(TEST-COUNT-VOWELS): (= (COUNT-VOWELS ) 0)
pass ...(TEST-COUNT-VOWELS): (= (COUNT-VOWELS b) 0)
pass ...(TEST-COUNT-VOWELS): (= (COUNT-VOWELS Assdva) 1)
T

More on assertions

  • The last step in writing a test is to validate the output against a known response (this is called an assertion)
  • Best practices:
    • Make sure tests are repeatable.
    • Try and assert results that relate to your input data.

The example below shows assertions for function HAS-VOWELS which returns T its input string contains at least one vowel.

(deftest test-has-vowels ()
  (check (not (has-vowels ""))
         (has-vowels "b")
         (has-vowels "bcde")))
RTL-USER> (test-has-vowels)
pass ...(TEST-HAS-VOWELS): (NOT (HAS-VOWELS ))
fail ...(TEST-HAS-VOWELS): (HAS-VOWELS b)
pass ...(TEST-HAS-VOWELS): (HAS-VOWELS bcde)
T