-
Notifications
You must be signed in to change notification settings - Fork 19
Home
lisp-unit is a Common Lisp library that supports unit testing. It is an extension of the library written by Chris Riesbeck. There is a long history of testing packages in Lisp, usually called "regression" testers. More recent packages in Lisp and other languages have been inspired by JUnit for Java.
The main goal for lisp-unit was to make it simple to use. The advantages of lisp-unit are:
- Written in portable Common Lisp.
- Usable by loading a single file.
- Extensions loadable with Quicklisp or ASDF.
- Dead-simple to define and run tests.
- Supports redefining functions and even macros without reloading tests.
- Supports test-first programming.
- Supports testing return values, printed output, macro expansions, and error conditions.
- Produces short readable output with a reasonable level of detail.
- Groups tests by package for modularity.
The core definitions of lisp-unit may be used by loading the single file 'lisp-unit.lisp'. To use the extensions, lisp-unit must be loaded using either Quicklisp] or ASDF.
- Load (or compile and load) as a single file :
(load "lisp-unit")
. - Load using Quicklisp :
(ql:quickload :lisp-unit)
. - Load using ASDF :
(asdf:load-system :lisp-unit)
.
Once lisp-unit has been loaded, load the file of unit-tests and run
the tests with run-tests
. Any test failures will be printed, along
with a summary of how many tests were run, how many passed, and how
many failed.
You define a test with define-test
:
(define-test name exp1 exp2 ...)
This defines a test called name
. The expressions can be anything,
but typically most will be assertion forms. Tests can be defined
before the code they test, even if they're testing macros. This is to
support test-first programming.
After defining your tests and the code they test, run the tests with
(run-tests)
This runs every test defined in the current package. To run just certain specific tests, use
(run-tests name1 name2 ...)
e.g., (run-tests greater summit)
.
The following example
- defines some tests to see if
my-max
returns the larger of two arguments - defines a deliberately broken version of
my-max
- runs the tests
First, we define some tests.
> (in-package :example)
#<PACKAGE EXAMPLE>
> (define-test test-my-max
(assert-equal 5 (my-max 2 5))
(assert-equal 5 (my-max 5 2))
(assert-equal 10 (my-max 10 10))
(assert-equal 0 (my-max -5 0)))
TEST-MY-MAX
Following good test-first programming practice, we run these tests before writing any code.
> (run-tests test-my-max)
TEST-MY-MAX: Undefined function MY-MAX called with arguments (2 5).
This shows that we need to do some work. So we define our broken version of my-max.
> (defun my-max (x y) x) ;; deliberately wrong
MY-MAX
Now we run the tests again:
> (run-tests my-max)
MY-MAX: (MY-MAX 2 5) failed: Expected 5 but saw 2
MY-MAX: (MY-MAX -5 0) failed: Expected 0 but saw -5
MY-MAX: 2 assertions passed, 2 failed.
This shows two failures. In both cases, the equality test returned
NIL
. In the first case it was because (my-max 2 5)
returned 2 when
5 was expected, and in the second case, it was because (my-max -5 0)
returned -5 when 0 was expected.
The most commonly used assertion form is
(assert-equal value form)
This tallies a failure if form returns a value not equal to value. Both value and test are evaluated in the local lexical environment. This means that you can use local variables in tests. In particular, you can write loops that run many tests at once:
> (define-test my-sqrt
(dotimes (i 5)
(assert-equal i (my-sqrt (* i i)))))
MY-SQRT
> (defun my-sqrt (n) (/ n 2)) ;; wrong!!
> (run-tests my-sqrt)
MY-SQRT: (MY-SQRT (* I I)) failed: Expected 1 but saw 1/2
MY-SQRT: (MY-SQRT (* I I)) failed: Expected 3 but saw 9/2
MY-SQRT: (MY-SQRT (* I I)) failed: Expected 4 but saw 8
MY-SQRT: 2 assertions passed, 3 failed.
However, the above output doesn't tell us for which values of i the code failed. Fortunately, you can fix this by adding expressions at the end of the assert-equal. These expression and their values will be printed on failure.
> (define-test my-sqrt
(dotimes (i 5)
(assert-equal i (my-sqrt (* i i)) i))) ;; added i at the end
MY-SQRT
> (run-tests my-sqrt)
MY-SQRT: (MY-SQRT (* I I)) failed: Expected 1 but saw 1/2
I => 1
MY-SQRT: (MY-SQRT (* I I)) failed: Expected 3 but saw 9/2
I => 3
MY-SQRT: (MY-SQRT (* I I)) failed: Expected 4 but saw 8
I => 4
MY-SQRT: 2 assertions passed, 3 failed.
The next most useful assertion form is
(assert-true test)
This tallies a failure if test returns false. Again, if you need to print out extra information, just add expressions after test. There are also assertion forms to test what code prints, what errors code returns, or what a macro expands into. A complete list of assertion forms is in the reference and extensions sections.
Do not confuse
assert-true
with Common Lisp'sassert
macro.assert
is used in code to guarantee that some condition is true. If it isn't, the code halts.assert
has options you can use to let a user fix what's wrong and resume execution. A similar collision of names exists in JUnit and Java.
Tests are grouped internally by the current package, so that a set of
tests can be defined for one package of code without interfering with
tests for other packages. If your code is being defined in cl-user
,
which is common when learning Common Lisp, but not for
production-level code, then you should define your tests in cl-user
as well. If your code is being defined in its own package, you should
define your tests either in that same package, or in another package
for test code. The latter approach has the advantage of making sure
that your tests have access to only the exported symbols of your code
package.
For example, if you were defining a date package, your date.lisp file would look like this:
(defpackage :date
(:use :common-lisp)
(:export :date->string :string->date))
(in-package :date)
(defun date->string (date) ...)
(defun string->date (string) ...)
Your date-tests.lisp file would look like this:
(defpackage :date-tests
(:use :common-lisp :lisp-unit :date))
(in-package :date-tests)
(define-test date->string
(assert-true (string= ... (date->string ...)))
...)
...
You could then run all your date tests in the test package:
(in-package :date-tests)
(run-tests)
Alternately, you could run all your date tests from any package with:
(lisp-unit:run-all-tests :date-tests)