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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
|
;;; test_is.clj: test framework for Clojure
;; by Stuart Sierra, http://stuartsierra.com/
;; December 3, 2008
;; Thanks to Chas Emerick, Allen Rohner, and Stuart Halloway for
;; contributions and suggestions.
;; Copyright (c) Stuart Sierra, 2008. All rights reserved. The use
;; and distribution terms for this software are covered by the Eclipse
;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;; which can be found in the file epl-v10.html at the root of this
;; distribution. By using this software in any fashion, you are
;; agreeing to be bound by the terms of this license. You must not
;; remove this notice, or any other, from this software.
(comment
;; Inspired by many Common Lisp test frameworks and clojure/test,
;; this file is a Clojure test framework.
;;
;; Define tests as :test metadata on your fns. Use the "is" macro
;; for assertions. Examples:
(defn add2
([x] (+ x 2))
{:test (fn [] (is (= (add2 3) 5))
(is (= (add2 -4) -2)
(is (> (add2 50) 50))))})
;; You can also define tests in isolation with the "deftest" macro:
(deftest test-new-fn
(is (= (new-fn) "Awesome")))
;; You can test that a function throws an exception with the
;; "is thrown?" form:
(defn factorial
([n] (cond
(zero? n) 1 ; 0!=1 is often defined for convenience
(> n 0) (* n (factorial (dec n)))
:else (throw (IllegalArgumentException. "Negative factorial"))))
{:test (fn [] (is (= (factorial 3) 6))
(is (= (factorial 6) 720))
(is (thrown? IllegalArgumentException (factorial -2))))})
;; Run tests with (run-tests). As in any language with macros, you
;; may need to recompile functions after changing a macro
;; definition.
;;
;; If you want write a bunch of tests with the same predicate, use
;; "are", which takes a template and applies it inside "is".
;;
;; Examples:
(deftest test-addition
(are (= _1 _2)
3 (+ 2 1)
4 (+ 2 2)
5 (+ 4 1)))
(deftest test-predicates
(are _ ;; the template is just an underscore
(true? true)
(false? false)
(nil? nil)))
) ;; end comment block
(ns clojure.contrib.test-is
(:require [clojure.contrib.template :as temp]))
(def *report-counters* nil) ; bound to a ref of a map in test-ns
(def *testing-vars* (list)) ; bound to hierarchy of vars being tested
(def *testing-contexts* (list)) ; bound to strings of test contexts
(defonce *load-tests* true) ; if false, deftest is ignored
;;; REPORTING METHODS
;; These are used in assert-expr methods. Rebind "report" to plug in
;; your own test-reporting framework.
(defn file-position
"Returns a vector [filename line-number] for the nth call up the
stack."
[n]
(let [s (nth (.getStackTrace (new java.lang.Throwable)) n)]
[(.getFileName s) (.getLineNumber s)]))
(defn testing-vars-str
"Returns a string representation of the current test. Renders names
in *testing-vars* as a list, then the source file and line of
current assertion."
[]
(let [[file line] (file-position 4)]
(str ;;(ns-name (:ns (meta (first *testing-vars*)))) "/ "
(reverse (map #(:name (meta %)) *testing-vars*))
" (" file ":" line ")")))
(defn testing-contexts-str
"Returns a string representation of the current test context. Joins
strings in *testing-contexts* with spaces."
[]
(apply str (interpose " " (reverse *testing-contexts*))))
(defn report-count
"Increments the named counter in *report-counters*."
[name]
(when *report-counters*
(dosync (commute *report-counters* assoc name
(inc (or (*report-counters* name) 0))))))
(defmulti report (fn [event msg expected actual] event))
(defmethod report :info [event msg expected actual]
(newline)
(println msg))
(defmethod report :pass [event msg expected actual]
(report-count :pass))
(defmethod report :fail [event msg expected actual]
(report-count :fail)
(println "\nFAIL in" (testing-vars-str))
(when (seq *testing-contexts*) (println (testing-contexts-str)))
(when msg (println msg))
(println "expected:" (pr-str expected))
(println " actual:" (pr-str actual)))
(defmethod report :error [event msg expected actual]
(report-count :error)
(println "\nERROR in" (testing-vars-str))
(when (seq *testing-contexts*) (println (testing-contexts-str)))
(when msg (println msg))
(println "expected:" (pr-str expected))
(println " actual:" (pr-str actual)))
;;; ASSERTION METHODS
;; You don't call these, but you can add methods to extend the 'is'
;; macro. These define different kinds of tests, based on the first
;; symbol in the test expression.
(defmulti assert-expr
(fn [msg form]
(cond
(nil? form) :always-fail
(seq? form) (first form)
:else :default)))
(defmethod assert-expr :default [msg form]
;; Default test: evaluate the form (which may be a bare symbol), and
;; pass if it is logical true.
`(let [value# ~form]
(if value#
(report :pass ~msg '~form value#)
(report :fail ~msg '~form value#))
value#))
(defmethod assert-expr :always-fail [msg form]
;; nil test: always fail
`(report :fail ~msg nil nil))
(defmethod assert-expr '= [msg form]
;; Equality test. Doesn't care about argument order:
;; (is (= expected actual)) or (is (= actual expected))
`(let [values# (list ~@(rest form))]
(let [result# (apply = values#)]
(if result#
(report :pass ~msg '~form (cons '~'= values#))
(report :fail ~msg '~form (cons '~'not= values#)))
result#)))
(defmethod assert-expr 'instance? [msg form]
;; Test if x is an instance of y.
`(let [klass# ~(nth form 1)
object# ~(nth form 2)]
(let [result# (instance? klass# object#)]
(if result#
(report :pass ~msg '~form (class object#))
(report :fail ~msg '~form (class object#)))
result#)))
(defmethod assert-expr 'thrown? [msg form]
;; (is (thrown? c expr))
;; Asserts that evaluating expr throws an exception of class c.
;; Returns the exception thrown.
(let [klass (second form)
body (rrest form)]
`(try ~@body
(report :fail ~msg '~form nil)
(catch ~klass e#
(report :pass ~msg '~form e#)
e#))))
;; New assertions coming soon:
;; * thrown-with-msg?
;; * re-matches
;;; CATCHING UNEXPECTED EXCEPTIONS
(defmacro try-expr
"Used by the 'is' macro to catch unexpected exceptions.
You don't call this."
[msg form]
`(try ~(assert-expr msg form)
(catch Throwable t#
(report :error ~msg '~form t#))))
;;; ASSERTION MACROS
;; you use these in your tests
(defmacro is
"Generic assertion macro. 'form' is any predicate test.
'msg' is an optional message to attach to the assertion.
Example: (is (= 4 (+ 2 2)) \"Two plus two should be 4\")
Special form (is (thrown? c body)) checks that an instance of c is
thrown from body, fails if not; then returns the thing thrown."
([form] `(is ~form nil))
([form msg] `(try-expr ~msg ~form)))
(defmacro are
"Checks multiple assertions with a template expression.
Example: (are (= _1 _2) 2 (+ 1 1), 4 (+ 2 2))
See clojure.contrib.template for documentation of templates."
[expr & args]
`(temp/do-template (is ~expr) ~@args))
(defmacro testing
"Adds a new string to the list of testing contexts. May be nested,
but must occur inside a test function (deftest)."
[string & body]
`(binding [*testing-contexts* (conj *testing-contexts* ~string)]
~@body))
;;; DEFINING TESTS INDEPENDENT OF FUNCTIONS
(defmacro deftest
"Defines a test function with no arguments. Test functions may call
other tests, so tests may be composed. If you compose tests, you
should also define a function named test-ns-hook; run-tests will
call test-ns-hook instead of testing all vars.
Note: Actually, the test body goes in the :test metadata on the var,
and the real function (the value of the var) calls test-var on
itself.
When *load-tests* is false, deftest is ignored."
[name & body]
(when *load-tests*
`(def ~(with-meta name {:test `(fn [] ~@body)})
(fn [] (test-var (var ~name))))))
(defmacro set-test
"Experimental.
Sets :test metadata of the named var to a fn with the given body.
The var must already exist. Does not modify the value of the var.
When *load-tests* is false, set-test is ignored."
[name & body]
(when *load-tests*
`(alter-meta! (var ~name) assoc :test (fn [] ~@body))))
(defmacro with-test
"Experimental.
Takes any definition form (that returns a Var) as the first argument.
Remaining body goes in the :test metadata function for that Var.
When *load-tests* is false, only evaluates the definition, ignoring
the tests."
[definition & body]
(if *load-tests*
`(doto ~definition (alter-meta! assoc :test (fn [] ~@body)))
definition))
;;; RUNNING TESTS
(defn test-var
"If v has a function in its :test metadata, calls that function,
with *testing-vars* bound to (conj *testing-vars* v)."
[v]
(when-let [t (:test (meta v))]
(binding [*testing-vars* (conj *testing-vars* v)]
(report-count :test)
(try (t)
(catch Throwable e
(report :error "Uncaught exception, not in assertion."
nil e))))))
(defn test-ns
"If the namespace defines a function named test-ns-hook, calls that.
Otherwise, calls test-var on all vars in the namespace. Returns a
map of counts for :test, :pass, :fail, and :error results."
[ns]
(binding [*report-counters* (ref {:test 0, :pass 0,
:fail 0, :error 0})]
(let [
|