├── .gitignore ├── .travis.yml ├── README.md ├── project.clj ├── resources └── test.html ├── src └── cljs_test │ ├── core.cljs │ └── macros.clj └── test └── cljs_test └── core_test.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | target/** 4 | pom.* 5 | .lein-repl-history 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | lein: lein2 3 | script: lein2 cljsbuild test 4 | jdk: 5 | - openjdk7 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **UPDATE 3/18/2015: this library is no longer under active development -- we'd recommend checking out [clojurescript.test](https://github.com/cemerick/clojurescript.test) or ClojureScript's new built-in [cljs.test](https://groups.google.com/forum/#!topic/clojure/gnCl0CySSk8)** 2 | 3 | # cljs-test [](http://travis-ci.org/Prismatic/cljs-test) 4 | 5 | 6 | Simple testing library for ClojureScript, mirroring `clojure.test` as much as possible. Each `deftest` runs after declaration and prints test statistics to console. Intended usage is with [phantomJS](http://phantomjs.org/) and `lein cljsbuild test` to get a readable test summary. In the future, we'll add some HTML scaffolding to support visual test results in a browser. 7 | 8 | Note: This is an alpha release and much left to do, will add things like support tests with asynchronous elements (`defasynctest`) which take completion callbacks. Also visual HTML representation. 9 | 10 | ## Usage 11 | 12 | ```clojure 13 | (ns mytest-ns 14 | (:require cljs-test.core) 15 | (:use-macros [cljs-test.macros :only [deftest is= is]])) 16 | 17 | (deftest simple-case 18 | (is= 1 (+ 0 1)) 19 | (is true) 20 | (is nil)) 21 | ``` 22 | 23 | Add a section in your ```project.clj``` as follows: 24 | 25 | ```clojure 26 | {:builds 27 | {:test {:source-paths ["src" "test"] 28 | :compiler {:output-to "target/unit-test.js" 29 | :optimizations :whitespace 30 | :pretty-print true}}} 31 | :test-commands {"unit" ["phantomjs" "target/unit-test.js"]}} 32 | ``` 33 | ### Testing in the browser 34 | 35 | To generate the test runner, 36 | 37 | $ lein clean 38 | $ lein cljsbuild once test 39 | 40 | Add an HTML file (recommended ```resources/test.html```) with the following content: 41 | 42 | ```html 43 | 44 |
45 | 46 | 47 | 48 | ``` 49 | Open the HTML file in a browser to execute the tests. 50 | 51 | ### Testing using PhantomJS 52 | 53 | Run 54 | 55 | $ lein clean 56 | $ lein cljsbuild test 57 | 58 | ## License 59 | 60 | Copyright (C) 2013 Prismatic. Distributed under the Eclipse Public License, the same as Clojure. 61 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject prismatic/cljs-test "0.0.7-SNAPSHOT" 2 | :clojurescript? true 3 | :description "Very simple cljs testing" 4 | :url "https://github.com/plumatic/cljs-test" 5 | :license {:name "Eclipse Public License" 6 | :url "http://www.eclipse.org/legal/epl-v10.html"} 7 | :cljsbuild 8 | {:builds 9 | {:test {:source-paths ["src" "test"] 10 | :compiler {:output-to "target/unit-test.js" 11 | :optimizations :whitespace 12 | :pretty-print true}}} 13 | :test-commands {"unit" ["phantomjs" "target/unit-test.js"]}} 14 | :dependencies [[org.clojure/clojurescript "0.0-2120"]] 15 | :plugins [[lein-cljsbuild "1.0.0"]]) 16 | -------------------------------------------------------------------------------- /resources/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/cljs_test/core.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-test.core 2 | (:use-macros 3 | [cljs-test.macros :only [is]])) 4 | 5 | (def tests (atom {})) 6 | (def +current-test+ (atom nil)) 7 | 8 | (defn add-test! [name f] 9 | (swap! tests assoc name 10 | {:fn f 11 | :stats 12 | {:total 0 13 | :pass 0 14 | :fail 0 15 | :error 0}})) 16 | 17 | (defn update-test-stats! [state] 18 | (swap! tests update-in [@+current-test+ :stats :total] inc) 19 | (swap! tests update-in [@+current-test+ :stats state] inc)) 20 | 21 | (defn test-stats [] 22 | (get-in @tests [@+current-test+ :stats])) 23 | 24 | (defn assertion-state [[error pass?]] 25 | (if (= :no-error error) 26 | (if pass? :pass :fail) 27 | :error)) 28 | 29 | (defn style [el m] 30 | (doseq [[k v] m] 31 | (aset (.-style el) (name k) v))) 32 | 33 | (def +state-style+ 34 | {:pass {:color "#67f86f"} 35 | :notice {:color "#77d3ee" :font-size "1.2em" :margin-top "1em" :font-weight "bold"} 36 | :fail {:color "#fff" :font-weight "bold"} 37 | :error {:color "#fff"} 38 | :debug {:color "#c5c5c5" :font-style "italic"}}) 39 | 40 | (def +ansi-style+ 41 | {:pass "\u001b[32m\u001b[1m" ; bright green 42 | :notice "\u001b[36m\u001b[1m" ; bright cyan 43 | :fail "\u001b[31m\u001b[1m" ; bright red 44 | :error "\u001b[31m\u001b[1m" ; bright red 45 | :debug "\u001b[37m\u001b[3m" ; italic grey 46 | :default "\u001b[39m\u001b[0m"}) 47 | 48 | (defn test-state 49 | [{:keys [pass, fail, error]}] 50 | (if (and (zero? fail) (zero? error)) 51 | :pass 52 | :fail)) 53 | 54 | (defn failed-tests [all-tests] 55 | (for [[test-name {:keys [stats]}] all-tests 56 | :when (= (test-state stats) :fail)] 57 | [test-name stats])) 58 | 59 | (defn html-report [] 60 | (let [el (.createElement js/document "div") 61 | all-tests @tests 62 | passed-tests (for [[test-name {:keys [stats]}] all-tests 63 | :when (= (test-state stats) :pass)] 64 | [test-name stats]) 65 | failed-tests (failed-tests all-tests) 66 | overview-el (.createElement js/document "div")] 67 | (style el {:position "fixed" 68 | :top "10px" 69 | :right "10px" 70 | :padding "1em" 71 | :max-width "50%" 72 | :background "#111"}) 73 | (style overview-el {:font-size "1.2em" 74 | :font-weight "bold" 75 | :color (if (empty? failed-tests) "#67f86f" "#d02f2f")}) 76 | (set! (.-innerHTML overview-el) 77 | (if-not (empty? failed-tests) 78 | (str (count failed-tests) " / " (count all-tests) " tests failed") 79 | "All tests passed!")) 80 | (.appendChild el overview-el) 81 | (doseq [[test-name stats] failed-tests 82 | :let [test-el (.createElement js/document "a")]] 83 | (.setAttribute test-el "href" (str "#" test-name)) 84 | (set! (.-innerHTML test-el) test-name) 85 | (style test-el {:color "#d02f2f"}) 86 | (.appendChild el test-el)) 87 | (.appendChild js/document.body el))) 88 | 89 | (defn ansi-header [type msg] 90 | (when type 91 | (str 92 | (+ansi-style+ type) 93 | (condp = type 94 | :fail " FAIL " 95 | :error " ERROR " 96 | :pass " PASS " 97 | :debug msg 98 | :notice msg) 99 | (+ansi-style+ :default) 100 | (when-not (#{:notice :debug} type) msg)))) 101 | 102 | (defn console-logger [type msg] 103 | (when js/window.phantom 104 | (.log 105 | js/console 106 | (.replace 107 | (ansi-header type msg) 108 | (js/RegExp. "\n" "g") 109 | "\n ")))) 110 | 111 | (defn log [type & more] 112 | (let [msg (.trimRight (apply str more))] 113 | (when (pos? (count msg)) 114 | (console-logger type msg) 115 | (let [el (.createElement js/document "div")] 116 | (style el (merge {:white-space "pre"} 117 | (+state-style+ type))) 118 | (set! (.-innerHTML el) msg) 119 | (.appendChild js/document.body el))))) 120 | 121 | (defn log-state [state msg] 122 | (console-logger state msg) 123 | (let [el (.createElement js/document "div") 124 | state-span (.createElement js/document "span")] 125 | (style el 126 | (merge {:white-space "pre" 127 | :padding-bottom "0.2em"} 128 | (when (#{:fail :error} state) 129 | {:background-color "#d02f2f"}))) 130 | (style state-span (merge {:display "inline-block" 131 | :text-transform "uppercase" 132 | :width "3.5em" 133 | :text-align "right" 134 | :margin-right "1em"} 135 | (+state-style+ state))) 136 | (set! (.-innerText state-span) (name state)) 137 | (.appendChild el state-span) 138 | (.appendChild el (.createTextNode js/document msg)) 139 | (.appendChild js/document.body el))) 140 | 141 | (set! 142 | *print-fn* 143 | (fn [s] 144 | (log :debug s))) 145 | 146 | (defn run-tests! [] 147 | (style js/document.body 148 | {:background "#252525" 149 | :color "#c7c7c7" 150 | :font "14px monospace"}) 151 | (doseq [[name test] @tests 152 | :let [bookmark (.createElement js/document "a")]] 153 | (reset! +current-test+ name) 154 | (.setAttribute bookmark "name" name) 155 | (.appendChild js/document.body bookmark) 156 | (log :notice name) 157 | (is (do ((:fn test)) true))) 158 | (html-report) 159 | (when js/window.phantom 160 | (let [failed-tests (failed-tests @tests)] 161 | (js/console.log (count failed-tests) "tests failed") 162 | (.exit js/window.phantom (if (empty? failed-tests) 0 1))))) 163 | 164 | (if js/window.phantom 165 | (.setTimeout js/window run-tests! 4000) 166 | (set! (.-onreadystatechange js/document) 167 | (fn [] 168 | (when (identical? "complete" (.-readyState js/document)) 169 | (run-tests!))))) -------------------------------------------------------------------------------- /src/cljs_test/macros.clj: -------------------------------------------------------------------------------- 1 | (ns cljs-test.macros 2 | "Simple Test library for ClojureScript. Runs each deftest after declaration and prints test statistics to console 3 | 4 | TODO: Handle async tests (defasynctest) which take completion callback 5 | TODO: Handle detecting tests have finished in phantom case and exit with return value 6 | TODO: Provide HTML scaffold to pretty display test results in browser" 7 | (:require [clojure.java.io :as io] 8 | [clojure.template :as template])) 9 | 10 | (defmacro read-json 11 | "read json from classpath, useful for test resources" 12 | [f] 13 | `(js/JSON.parse ~(slurp f))) 14 | 15 | (defmacro read-clj 16 | "read clojure literal which is valid cljs" 17 | [f] 18 | ~(read (slurp f))) 19 | 20 | (defmacro deftest 21 | [nm & body] 22 | `(cljs-test.core/add-test! ~(name nm) (fn [] ~@body))) 23 | 24 | (defmacro safe-eval [expr] 25 | (let [ex-sym (gensym "exception")] 26 | `(try 27 | [:no-error ~expr] 28 | (catch js/Error ~ex-sym 29 | (cljs-test.core/log :error (.-stack ~ex-sym)) 30 | [:error ~ex-sym])))) 31 | 32 | (defmacro is 33 | ([expr] 34 | `(is ~expr ~(str expr))) 35 | ([expr msg] 36 | `(let [as# (cljs-test.core/assertion-state (safe-eval ~expr))] 37 | (cljs-test.core/update-test-stats! as#) 38 | (cljs-test.core/log-state as# ~msg)))) 39 | 40 | (defmacro is-thrown? 41 | ([expr] 42 | `(is-thrown? ~expr ~(str expr))) 43 | ([expr msg] 44 | `(is 45 | (try (do ~expr false) (catch js/Error _# true)) 46 | ~msg))) 47 | 48 | (defmacro is= 49 | ([a b] 50 | (let [msg (str "(= " `~a " " `~b ")")] 51 | `(is= ~a ~b ~msg))) 52 | ([a b msg] 53 | `(let [lhs# (safe-eval ~a) 54 | rhs# (safe-eval ~b) 55 | as# (case [(first lhs#) (first rhs#)] 56 | [:no-error :no-error] (cljs-test.core/assertion-state 57 | (safe-eval (= (second lhs#) (second rhs#)))) 58 | :error) 59 | msg# (if (= as# :fail) 60 | (str ~msg " (not= " (second lhs#) " " (second rhs#) ")") 61 | ~msg)] 62 | (cljs-test.core/update-test-stats! as#) 63 | (cljs-test.core/log-state as# msg#)))) 64 | 65 | (defmacro are 66 | [argv expr & args] 67 | (if (or 68 | ;; (are [] true) is meaningless but ok 69 | (and (empty? argv) (empty? args)) 70 | ;; Catch wrong number of args 71 | (and (pos? (count argv)) 72 | (pos? (count args)) 73 | (zero? (mod (count args) (count argv))))) 74 | `(template/do-template ~argv (is ~expr) ~@args) 75 | (throw (IllegalArgumentException. "The number of args doesn't match are's argv.")))) 76 | -------------------------------------------------------------------------------- /test/cljs_test/core_test.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-test.core-test 2 | (:use-macros 3 | [cljs-test.macros :only [deftest is= is is-thrown? are]]) 4 | (:require 5 | [cljs-test.core :as test])) 6 | 7 | (deftest test 8 | (is= {:pass 0 :fail 0 :total 0 :error 0} (test/test-stats)) 9 | (is 1) 10 | (is= {:pass 2 :fail 0 :total 2 :error 0} (test/test-stats)) 11 | (test/log :debug "Some debug logging with " 3 " args") 12 | (is nil "Expected fail") 13 | (is= 5 (+ 2 2) "Expected fail - show actual & expected") 14 | (is= {:pass 3 :fail 2 :total 5 :error 0} (test/test-stats)) 15 | (is (.makeUpFn (js-obj)) "Expected exception") 16 | (is= {:pass 4 :fail 2 :total 7 :error 1} (test/test-stats)) 17 | (is-thrown? (+ 2 3) "Expected fail") 18 | (is= {:pass 5 :fail 3 :total 9 :error 1} (test/test-stats)) 19 | (is-thrown? (throw (js/Error. "pwned"))) 20 | (is= {:pass 7 :fail 3 :total 11 :error 1} (test/test-stats)) 21 | (println "Some captured println logging") 22 | (are [x y] (zero? (mod x y)) 23 | 4 2 24 | 9 3) 25 | (is= {:pass 10 :fail 3 :total 14 :error 1} (test/test-stats)) 26 | (if (= {:pass 11 :fail 3 :total 15 :error 1} (test/test-stats)) 27 | (swap! test/tests assoc-in [@test/+current-test+ :stats] 28 | {:pass 11 :fail 0 :total 11 :error 0}))) 29 | --------------------------------------------------------------------------------