TDD system for Common Lisp
for the your lisp forms.
testiere, you embed test expressions directly into your
code. When you compile, those tests are run. If any tests fail, you
are dropped into the debugger where you can decide what to do.
This approach has several beneifts:
- *Does Not Add Dependencies* You do not need to add
testiereasa dependency to your project. It is enough to load
testiereintoyour Lisp image and evoke
- *TDD* Common Lisp is a language well suited to interactivedevelopment. Why should testing be any different? With
testiereyou can test functions as you
C-c C-cthem in SLIME, or wheneveryou load or compile a file.
- *Self Documentation* Because tests are in the source (but do notend up compiled into executable code unless
testiereis "on"),you get purposeful documentation of your code for free. Why read acomment when there's a test!?
Out of the box,
testiere supports testing of the following:
1.1A Basic Example
(defun add3 (x y z) "Adds three numbers" #+testiere (:tests (= 6 (add3 1 2 3)) (:fails (add3 "hey")) (:fails (add3 1 2))) (+ x y z))
This compiles as normal. If you wish to run the tests in the
(:tests ...) form, however, you need to turn testiere on.
Now if you try recompiling
add3 those tests will be run.
This approach lets you add tests to functions without actually including the testiere source in your distributed code. You need only have testiere loaded and turned on during development.
You can, of course, turn testiere off too:
Within the body of a
(:tests ...) form are test expressions.
||The test fails if
|error messages than
||Calls a function with some arguments. If this|
|function signals an error, then the test fails.|
|Useful when running several complext tests.|
|If it does not signal an error, then the test fails.|
|condition of type
|the test fails.|
||Runs test expressions in the context of some bound|
|functions will which have temporary definitions for|
|the duration of the
||Temporarily redefine the an entire generic|
|function for the duration of the enclosed|
|is essentially anything that normally follows|
(defpackage :testiere.examples (:use #:cl #:testiere)) (defpackage :dummy (:use #:cl)) (in-package :testiere.examples) ;;; Turn Testiere On. (testiere:on) ;;; BASIC TESTS (defun add3 (x y z) "Adds three numbers" #+testiere (:tests (= 6 (add3 1 2 3)) (:is (evenp (add3 2 2 2))) (:fails (add3)) (:fails (add3 1 2 "oh no"))) (+ x y z)) ;;; Using external tests (defun dummy::test-add10 (n) "Tests add10 in the same way N times. Obviously useless. We define this in a separate package to give you an idea that you can embed tests that aren't part of the package you're testing." (loop :repeat n :do (assert (= 13 (add10 3))))) (defun add10 (x) "Adds 10 to X" #+testiere (:tests (:funcall 'dummy::test-add10 1)) (+ x 10)) ;;; Adding some context to tests with :LET (defvar *count*) (defun increment-count (&optional (amount 1)) "Increments *COUNT* by AMOUNT" #+testiere (:tests (:let ((*count* 5)) (:do (increment-count)) (= *count* 6) (:do (increment-count 4)) (= *count* 10)) (:let ((*count* -10)) (= (increment-count) -9))) (incf *count* amount)) ;;; Stubbing functions with :WITH-DEFUNS (defun dummy::make-drakma-request (url) "Assume this actually makes an HTTP request using drakma" ) (defun test-count-words-in-response () (assert (= 3 (count-words-in-response "blah")))) (defun count-words-in-response (url) "Fetches a url and counts the words in the response." #+testiere (:tests (:with-defuns ((dummy::make-drakma-request (url) "Hello there dudes")) (= 3 (count-words-in-response "dummy-url")) (:funcall 'test-count-words-in-response))) (loop :with resp string := (dummy::make-drakma-request url) :with in-word? := nil :for char :across resp :when (and in-word? (not (alphanumericp char))) :count 1 :into wc :and :do (setf in-word? nil) :when (alphanumericp char) :do (setf in-word? t) :finally (return (if (alphanumericp char) (1+ wc) wc)))) ;;; Testing Classes (defclass point () ((x :accessor px :initform 0 :initarg :x) (y :accessor py :initform 0 :initarg :y)) #+testiere (:tests (:let ((pt (make-instance 'point :x 10 :y 20))) (= 20 (py pt)) (= 10 (px pt)) (:is (< (px pt) (py pt)))))) ;;; Testing Structs (defstruct pt x y #+testiere (:tests (:let ((pt (make-pt :x 10 :y 20))) (= 20 (pt-y pt)) (:is (< (pt-x pt) (pt-y pt)))))) ;;; Testing Types (deftype optional-int () #+testiere (:tests (:is (typep nil 'optional-int)) (:is (typep 10 'optional-int)) (:is (not (typep "foo" 'optional-int)))) '(or integer null))
1.4How does it work?
Under the hood,
testiere defines a custom
consults a registry of hooks. If a macro is found in the registery,
tests are extracted and run whenever they appear. Otherwise the hook
expands code normally.
Users can register
testiere hooks by calling
testiere:register-hook on three arguments:
- A symbol naming a macro
- A function designator for a function that extracts tests from amacro call (from the
&wholeof a macro call), returning themodified form and a list of the extracted test expressions. All ofthe built-ins hooks use the
- An optional function accepting the same
&wholeof the macro call,and returning a list of restart handlers that are inserted as-isinto the body of a
Any macro that has been so registered will be available for testing at compile time.