diff options
Diffstat (limited to 'src/clojure/contrib/test_is.clj')
-rw-r--r-- | src/clojure/contrib/test_is.clj | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/src/clojure/contrib/test_is.clj b/src/clojure/contrib/test_is.clj new file mode 100644 index 00000000..8374b772 --- /dev/null +++ b/src/clojure/contrib/test_is.clj @@ -0,0 +1,238 @@ +;;; test_is.clj: test framework for Clojure + +;; by Stuart Sierra, http://stuartsierra.com/ +;; September 25, 2008 + +;; Thanks to Chas Emerick for contributions. +;; Thanks to Allen Rohner for assert-raises. + +;; Copyright (c) 2008 Stuart Sierra. All rights reserved. The use and +;; distribution terms for this software are covered by the Common +;; Public License 1.0 (http://www.opensource.org/licenses/cpl1.0.php) +;; which can be found in the file CPL.TXT 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 "throws" +;; macro: +;; +;; (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)) +;; (throws 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 + #^{:doc "PrintWriter to which test results are printed; defaults to + System.err."} + *test-out* (. System err)) + + +;;; PRIVATE + +(defmacro #^{:private true} defcounter [ref-name fn-name] + `(do (def ~(with-meta ref-name {:private true}) nil) + (defn ~fn-name [] + (when ~ref-name (sync nil (commute ~ref-name inc)))))) + +(defcounter *tests* count-test) +(defcounter *assertions* count-assertion) +(defcounter *failures* count-failure) +(defcounter *exceptions* count-exception) + +(defmacro failure [reason message] + `(throw (new java.lang.AssertionError + (str ~reason (when ~message (str "; " ~message)))))) + +(defn- assert-true [form message] + `(do (count-assertion) + (let [value# ~form] + (when-not value# + (failure (str ~(pr-str form) " was false/nil") + ~message))))) + +;; Multimethod for testing expressions, dispatches on the first symbol +;; in the expression. +(defmulti assert-expr (fn [form message] (first form))) + +;; Test for (= actual expected) expressions. +(defmethod assert-expr '= [form message] + (let [expr1 (second form) + expr2 (nth form 2)] + `(do (count-assertion) + (let [value1# ~expr1 + value2# ~expr2] + (when-not (= value1# value2#) + (failure (str ~(pr-str expr1) " is " (pr-str value1#) + " but should be " (pr-str value2#)) + ~message)))))) + +;; Test for (instance? class object) expressions. +(defmethod assert-expr 'instance? [form message] + (let [clazz (second form) + object (nth form 2)] + `(do (count-assertion) + (let [value1# ~clazz + value2# ~object] + (when-not (instance? value1# value2#) + (failure (str ~(pr-str object) " has " (class value2#) + " but should have " (pr-str value1#)) + ~message)))))) + +;; Generic expression test, just check if expression evaluates to +;; logical true. +(defmethod assert-expr :default [form message] + (assert-true form message)) + +(defn- always-fail-assert [message] + `(do (count-assertion) + (failure ~message nil))) + +(defmacro #^{:private true} with-test-counters + "Creates dynamic bindings for counting the number of tests, + assertions, failures, and exceptions. Returns the results in a + map." + [& body] + `(binding [*tests* (ref 0) + *assertions* (ref 0) + *failures* (ref 0) + *exceptions* (ref 0)] + ~@body + {:tests @*tests* + :assertions @*assertions* + :failures @*failures* + :exceptions @*exceptions*})) + +(defn- run-test-fn + "Calls the function; reports errors/exceptions." + [f name] + (try + (count-test) + (f) + (catch java.lang.AssertionError e + (count-failure) + (. *test-out* (println (str "FAIL in " name ": " + (.getMessage e))))) + (catch java.lang.Exception e + (count-exception) + (. *test-out* (println (str "EXCEPTION in " name ":"))) + (.printStackTrace e *test-out*)))) + +(defn- test-var + "Finds and calls the fn in a var's :test metadata." + [v] + (when-let [f (:test (meta v))] + (run-test-fn f (str v)))) + +(defn- test-interns + "Tests all interned symbols in the namespace." + [ns] + (let [ns (if (symbol? ns) (find-ns ns) ns)] + (. *test-out* (println (str "Testing " ns))) + (dorun (map test-var (vals (ns-interns ns)))))) + + +;;; PUBLIC + +(defmacro is + "Generic assertion macro. Throws AssertionError if form evaluates + logical false. Optional message will be added to the error. + + form may be one of: + * an equality test like (= expression expected-value) + * an instance? test like (instance? class expression) + * nil, which always fails + * an arbitrary expression, fails if it returns false/nil" + ([form] `(is ~form nil)) + ([form message] + (cond + (nil? form) (always-fail-assert message) + (seq? form) (assert-expr form message) + :else (assert-true form message)))) + +(defmacro throws + "Asserts that form throws an exception of the given class (or one of + its subclasses)." + ([class form] + `(throws ~class ~form nil)) + ([class form message] + `(try + (count-assertion) + (let [value# ~form] + (failure (str "expected " ~(pr-str form) " to throw " ~class + ", but returned " value#) ~message)) + (catch ~class e# nil) ; the correct exception was thrown + (catch java.lang.Throwable e# ; some other exception was thrown + (failure (str "expected " ~(pr-str form) " to throw " ~class + ", but threw " e#) ~message))))) + +(defn print-results + "Prints a summary of the results from test-ns to *test-out*." + [r] + (. *test-out* + (println (str "\nRan " (:tests r) " tests with " + (:assertions r) " assertions.\n" + (:failures r) " failures, " + (:exceptions r) " exceptions."))) ) + +(defn test-ns + "Runs tests on all interned symbols in the namespaces + (symbols or namespace objects). + + Returns a map with the following keys: + :tests => number of tests run + :assertions => number of assertions checked + :failures => number of failed assertions + :exceptions => number of exceptions raised + + If no namespace is given, uses *ns*." + ([] (test-ns *ns*)) + ([& namespaces] + (with-test-counters (dorun (map test-interns namespaces))))) + +(defn run-tests + "Runs tests in the given namespaces and prints a summary of + results. + + If no namespace is given, uses *ns*." + [& namespaces] + (print-results (apply test-ns namespaces))) + +(defn run-all-tests + "Runs tests in all namespaces and prints a summary of results." + [] + (apply run-tests (all-ns))) + +(defmacro deftest + "Defines a Var with no value and with body in its :test fn." + [name & body] + `(def ~(with-meta name {:test `(fn [] ~@body)}))) |