testiere
2023-02-15
Interactive Testing for DEFUN
testiere
is armorfor the head of your defun
forms.
1Testiere
With testiere
you can program in an interactive TDD-like
fashion. Tests are included at the top of a defun/t
form. When you
recompile your functions interactively, the tests are run. If any
fail, you are dropped into a debugger where you can decide to revert
the definition to the last known working version, or you can choose to
unbind it altogether.
The system supports mocking and stubbing in your tests, so that you can, e.g. test the system in different dynamic contexts or by mocking network request functions.
Here is an example:
(defun/t sum-3 (x y &key (z 10))
"Sums three numbers, Z has a default value of 10"
:tests
(:program some-test-function)
(= (1 2) 13) ; (sum-3 1 2) == 13
(= (1 2 :z 3) 6) ; (sum-3 1 2 :z 3) == 6
(:outputp (0 0) ; tests that (sum-3 0 0) passes the predicate
(lambda (result) (= 10 result)))
(:fails ; ensures that (sum-3 "strings" "ain't" :z "numbers") fails
("strings" "ain't" :z "numbers"))
:end
(+ x y z))
In the above, a function sum-3
is defined with five embedded
tests. The test specification syntax is detailed below. If any of the
tests fail, the function will not be redefined and you will drop into
the debugger, which asks you how you'd like to proceed.
The approach to TDD-like development taking by testiere
may not be
appropriate to all circumstances, but it is good for interactive
development of interactive applications (ὠ9) whose "main loop"
involves a good sized collection of unit-testable functions.
1.1Test Specification
There are a few kinds of tests available.
1.1.1For the Impatient, Just Use :program
Tests
Most users will probably benefit from the :program
style test. Here
is a quick example:
(defun test-fibble ()
(assert (= 13 (fibble 1 2))))
(defun/t fibble (x y &key (z 10))
"Adds three numbers, one of which defaults to 10."
:tests
(:program test-fibble)
:end
(+ x y z))
In the above test, we insist that the test-fibble
function not
signal an error condition in order for fibble
to be successfully
(re)compiled.
1.1.2Basic Test Specifications
A test suite is a list of forms that appear between :tests
and
:end
in the body of a defun/t
form. The test suite must appear
after any optional docstring and before the function body actually
begins.
A catalog of test form specifications follows.
1.1.2.1Comparator Test Specifications
(comparator (&rest args...) value)
The comparator
should be the name of a binary predicate (like <
or
eql
). These tests proceed by calling (comparator (apply my-fun args) value)
If the comparison fails, an error condition is signaled.
Amending the above example, we include a comparator test:
(defun/t fibble (x y &key (z 10))
"Adds three numbers, one of which defaults to 10."
:tests
(:program test-fibble)
(= (0 0 :z 30) 30) ; (assert (= (fibble 0 0 :z 30) 30))
:end
(+ x y z))
1.1.2.2Other Test Specifications
Every other form appearing in a test suite is a list that starts with a keyword.
(:program FUNCTION-NAME ARGS...)
runs a function namedFUNCTION-NAME with arguments ARGS. This function is meant to act asa test suite for the function being defined with defun/t. It maycall that function and ASSERT things about it.(:outputp (..ARGS...) PREDICATE)
asserts that the output passesthe one-argument predicate.(:afterp (...ARGS...) THUNK)
asserts that the thunk should returnnon-nil after the function has run. Good for testing values ofdynamic variables that the function might interact with.(:fails (...ARGS...))
asserts that the function will produce anerror with the given arguments.(:signals (...ARGS...) CONDITION)
whereCONDITION
is the name ofa condition. Asserts that the function will signal a condition ofthe supplied type when called with the provided arguments.
1.1.3Mocking and Stubbing
The following test forms allow for the running of tests inside a context in which certain functions or global values are bound:
Binding variables looks like
(:let LET-BINDINGS TESTS)and are useful for binding dynamic variables for use during a set oftests.
For example
(defvar *count*)
(defun/t increment-count ()
"Increments the *count* variable."
:tests
(:let ((*count* 4))
(:afterp () (lambda () (= *count* 5))) ; 5 after the first call
(= () 6) ; 6 after the second
(:outputp () (lambda (x) (= x 7)))) ; and 7 after the third
:end
(incf *count*))
The :with-stubs
form is similar, except that it binds temporaryvalues to functions that might be called by the form inquestions. Useful for mocking.
(defun just-a-function ()
(print "Just a function."))
(defun/t call-just-a-function ()
"Calls JUST-A-FUNCTION."
:tests
(:with-stubs ((just-a-function () (print "TEMP JUST-A-FUNCTION.")))
(equal () "TEMP JUST-A-FUNCTION."))
:end
(just-a-function))
In the above, the temporary redefinition of JUST-A-FUNCTION
is used.