aboutsummaryrefslogtreecommitdiff
path: root/src/clojure/contrib/test_is.clj
blob: b8eda12bdbf12840d3cac1b251ec9c3a12893def (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
;;; 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.


;; 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.


(ns clojure.contrib.test-is)


(def *report-counters* nil)  ; bound to a ref of a map in test-ns

(def *test-name* nil)  ; bound to the name of a var during testing



;;; REPORTING METHODS

;; These are used in assert-expr methods.  Rebind "report" to plug in
;; your own test-reporting framework.

(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" *test-name*)
  (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" *test-name* msg)
  (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 the same predicate.
  Expands to (is (pred p1 p2)) for each pair.

  Example: (are = 2 (+ 1 1), 4 (+ 2 2))"
  [pred & pairs]
  `(do ~@(map (fn [pair] (list 'is (cons pred pair)))
              (partition 2 pairs))))



;;; DEFINING TESTS INDEPENDENT OF FUNCTIONS

(defmacro deftest
  "Defines a Var with no value and with body in its :test fn."
  [name & body]
  `(def ~(with-meta name {:test `(fn [] ~@body)})))



;;; RUNNING TESTS

(defn test-var
  "If v has a function in its :test metadata, calls that function, with
  *test-name* bound to the name of the var."
  [v]
  (when-let [t (:test (meta v))]
      (binding [*test-name* (str v)]
        (report-count :test)
        (t))))

(defn test-ns
  "Tests 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 [ns (if (symbol? ns) (find-ns ns) ns)]
      (report :info (str "Testing " ns) nil nil)
      (doseq [v (vals (ns-interns ns))]
          (test-var v)))
    @*report-counters*))

(defn print-results
  "Prints formatted results message based on the reported
  counts in r."
  [r]
  (println "\nRan" (:test r) "tests containing"
           (+ (:pass r) (:fail r) (:error r)) "assertions.")
  (println (:fail r) "failures," (:error r) "errors."))

(defn run-tests
  "Runs all tests in the given namespaces; prints results.
  Defaults to current namespace if none given."
  ([] (run-tests *ns*))
  ([& namespaces]
      (print-results (apply merge-with + (map test-ns namespaces)))))

(defn run-all-tests
  "Runs all tests in all namespaces; prints results."
  []
  (apply run-tests (all-ns)))