aboutsummaryrefslogtreecommitdiff
path: root/README.org
blob: 0e4ca0c56f467abb72888fbcbd5911fd0b9d004e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
A [[https://en.wiktionary.org/wiki/testiere][testiere]] is armor for the head of a horse and ~testiere~ is armor
for the your lisp forms.

* Testiere

With ~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:

1. **Does Not Add Dependencies** You do not need to add ~testiere~ as
   a dependency to your project. It is enough to load ~testiere~ into
   your Lisp image and evoke ~(testiere:on)~. 
2. **TDD** Common Lisp is a language well suited to interactive
   development. Why should testing be any different? With ~testiere~
   you can test functions as you =C-c C-c= them in SLIME, or whenever
   you load or compile a file.
3. **Self Documentation** Because tests are in the source (but do not
   end up compiled into executable code unless ~testiere~ is "on"),
   you get purposeful documentation of your code for free.  Why read a
   comment when there's a test!?

Out of the box, ~testiere~ supports testing of the following:

- ~defun~
- ~defmethod~
- ~deftype~
- ~defclass~
- ~defstruct~

** A Basic Example

#+begin_src lisp

(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))
  
#+end_src

This compiles as normal. If you wish to run the tests in the
~(:tests ...)~ form, however, you need to turn testiere on.

#+begin_src lisp

(testiere:testiere-on)

#+end_src

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:

#+begin_src lisp

(testiere:testiere-off)

#+end_src

** Tests Expressions

Within the body of a ~(:tests ...)~ form are test expressions.

| Expression                                         | Description                                    |
|----------------------------------------------------+------------------------------------------------|
| ~(:is form)~                                       | The test fails if ~form~ evaluates             |
|                                                    | to NIL.                                        |
|----------------------------------------------------+------------------------------------------------|
| ~(pred form1 form2)~                               | E.g  ~(= (foo) 10)~  Provides more             |
|                                                    | informative error messages than ~:is~          |
|----------------------------------------------------+------------------------------------------------|
| ~(:funcall function arg1 ...)~                     | Calls a function with some arguments.          |
|                                                    | If this function signals an error,             |
|                                                    | then the test fails. Useful when               |
|                                                    | running many or complex tests.                 |
|----------------------------------------------------+------------------------------------------------|
| ~(:fails form)~                                    | Evaluates ~form~ and expects it to             |
|                                                    | signal an error. If it does not                |
|                                                    | signal an error, the test fails.               |
|----------------------------------------------------+------------------------------------------------|
| ~(:signals condition form)~                        | Evaluates ~form~ and expects it to             |
|                                                    | signal a condition of type                     |
|                                                    | ~condition~. If it does not, then              |
|                                                    | the test fails.                                |
|----------------------------------------------------+------------------------------------------------|
| ~(:let bindings test1 ...)~                        | Runs test expressions in the context           |
|                                                    | of some bound variables.                       |
|----------------------------------------------------+------------------------------------------------|
| ~(:with-defuns ((name args body) ...) tests ... )~ | Mimics ~labels~ syntax. Used for               |
|                                                    | stubbing / mocking functions will which        |
|                                                    | have temporary definitions for the             |
|                                                    | duration of the ~:with-defuns~ form.           |
|----------------------------------------------------+------------------------------------------------|
| ~(:with-generic name methods tests ... )~          | Temporarily redefine the an entire generic     |
|                                                    | function for the duration of the enclosed      |
|                                                    | ~tests~. ~methods~ is a list of forms, each of |
|                                                    | is essentially anything  that normally follows |
|                                                    | ~(defmethod name ...)~.                        |
|                                                    | E.g. ~((x string) (string-upcase x))~ or       |
|                                                    | ~(:after (x string) (print "after"))~          |

** Examples

#+begin_src lisp
(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))
     (:funcall #'increment-count)
     (= *count* 6)
     (:funcall #'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)
                                    (declare (values (simple-array character)))
                                    "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))

#+end_src


** How does it work?

Under the hood, ~testiere~ defines a custom ~*macroexpand-hook*~ that
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.

** Extending

Users can register ~testiere~ hooks by calling
~testiere:register-hook~ on three arguments:

1. A symbol naming a macro
2. A function designator for a function that extracts tests from a
   macro call (from the ~&whole~ of a macro call), returning the
   modified form and a list of the extracted test expressions. All of
   the built-ins hooks use the ~testiere::standard-extractor~.
3. An optional function accepting the same ~&whole~ of the macro call,
   and returning a list of restart handlers that are inserted as-is
   into the body of a ~restart-case~.  See =src/standard-hooks.lisp=
   for examples.