Wrapping Up
You could keep going, adding more features to this test framework. But as a framework for writing tests with a minimum of busywork and easily running them from the REPL, this is a reasonable start. Here’s the complete code, all 26 lines of it:
(defvar *test-name* nil)
(defmacro deftest (name parameters &body body)
"Define a test function. Within a test function we can call
other test functions or use 'check' to run individual test
cases."
`(defun ,name ,parameters
(let ((*test-name* (append *test-name* (list ',name))))
,@body)))
(defmacro check (&body forms)
"Run each expression in 'forms' as a test case."
`(combine-results
,@(loop for f in forms collect `(report-result ,f ',f))))
(defmacro combine-results (&body forms)
"Combine the results (as booleans) of evaluating 'forms' in order."
(with-gensyms (result)
`(let ((,result t))
,@(loop for f in forms collect `(unless ,f (setf ,result nil)))
,result)))
(defun report-result (result form)
"Report the results of a single test case. Called by 'check'."
(format t "~:[FAIL~;pass~] ... ~a: ~a~%" result *test-name* form)
result)
It’s worth reviewing how you got here because it’s illustrative of how programming in Lisp often goes.
You started by defining a simple version of your problem—how to evaluate a bunch of boolean expressions and find out if they all returned true. Just **AND**
ing them together worked and was syntactically clean but revealed the need for better result reporting. So you wrote some really simpleminded code, chock-full of duplication and error-prone idioms that reported the results the way you wanted.
The next step was to see if you could refactor the second version into something as clean as the former. You started with a standard refactoring technique of extracting some code into a function, report-result
. Unfortunately, you could see that using report-result
was going to be tedious and error-prone since you had to pass the test expression twice, once for the value and once as quoted data. So you wrote the check
macro to automate the details of calling report-result
correctly.
While writing check
, you realized as long as you were generating code, you could make a single call to check
to generate multiple calls to report-result
, getting you back to a version of test-+
about as concise as the original **AND**
version.
At that point you had the check
API nailed down, which allowed you to start mucking with how it worked on the inside. The next task was to fix check
so the code it generated would return a boolean indicating whether all the test cases had passed. Rather than immediately hacking away at check
, you paused to indulge in a little language design by fantasy. What if—you fantasized—there was already a non-short-circuiting **AND**
construct. Then fixing check
would be trivial. Returning from fantasyland you realized there was no such construct but that you could write one in a few lines. After writing combine-results
, the fix to check
was indeed trivial.
At that point all that was left was to make a few more improvements to the way you reported test results. Once you started making changes to the test functions, you realized those functions represented a special category of function that deserved its own abstraction. So you wrote deftest
to abstract the pattern of code that turns a regular function into a test function.
With deftest
providing an abstraction barrier between the test definitions and the underlying machinery, you were able to enhance the result reporting without touching the test functions.
Now, with the basics of functions, variables, and macros mastered, and a little practical experience using them, you’re ready to start exploring Common Lisp’s rich standard library of functions and data types.
1This is for illustrative purposes only—obviously, writing test cases for built-in functions such as **+**
is a bit silly, since if such basic things aren’t working, the chances the tests will be running the way you expect is pretty slim. On the other hand, most Common Lisps are implemented largely in Common Lisp, so it’s not crazy to imagine writing test suites in Common Lisp to test the standard library functions.
2Side effects can include such things as signaling errors; I’ll discuss Common Lisp’s error handling system in Chapter 19. You may, after reading that chapter, want to think about how to incorporate tests that check whether a function does or does not signal a particular error in certain situations.
3I’ll discuss this and other **FORMAT**
directives in more detail in Chapter 18.
4If test-+
has been compiled—which may happen implicitly in certain Lisp implementations—you may need to reevaluate the definition of test-+
to get the changed definition of check
to affect the behavior of test-+
. Interpreted code, on the other hand, typically expands macros anew each time the code is interpreted, allowing the effects of macro redefinitions to be seen immediately.
5You have to change the test to make it fail since you can’t change the behavior of **+**
.
6Though, again, if the test functions have been compiled, you’ll have to recompile them after changing the macro.
7As you’ll see in Chapter 12, **APPEND**
ing to the end of a list isn’t the most efficient way to build a list. But for now this is sufficient—as long as the test hierarchies aren’t too deep, it should be fine. And if it becomes a problem, all you’ll have to do is change the definition of deftest
.