Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make checking's options optional #58

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ The `checking` macro is intended to be used with
'[com.gfredericks.test.chuck.clojure-test :refer [checking]])

(deftest my-test
(checking "that positive numbers are positive" 100
(checking "that positive numbers are positive"
[x gen/s-pos-int]
(is (pos? x))
(is (> x 0))))
Expand Down
55 changes: 36 additions & 19 deletions src/com/gfredericks/test/chuck/clojure_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,13 @@
(defmacro qc-and-report-exception
[final-reports num-tests-or-options bindings & body]
`(report-exception-or-shrunk
(let [num-tests-or-options# ~num-tests-or-options]
(apply tc/quick-check
(times num-tests-or-options#)
(prop/for-all ~bindings
(let [reports# (capture-reports ~@body)]
(swap! ~final-reports save-to-final-reports reports#)
(pass? reports#)))
(apply concat (options num-tests-or-options#))))))
(apply tc/quick-check
(times ~num-tests-or-options)
(prop/for-all ~bindings
(let [reports# (capture-reports ~@body)]
(swap! ~final-reports save-to-final-reports reports#)
(pass? reports#)))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please maintain the 2-space indentation for for-all expressions

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

(apply concat (options ~num-tests-or-options)))))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason there was a let clause at the top in the old code is that it lets you use num-tests-or-options in two places without inserting the code twice. This is general good-practice when writing a macro because otherwise if users have a nontrivial expression, it will be evaluated twice.

E.g., compare the expansions of (doubler (inc 20)) with these two definitions:

(defmacro doubler
  [x]
  `(+ ~x ~x))

(defmacro doubler
  [x]
  `(let [x# ~x] (+ x# x#)))

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I look closer, have you actually changed anything in this code, other than inlining num-tests-or-options? If not, then you should be able to just revert to the original code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never thought of the double evaluation. Thanks for the tip.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code reverted by the way


(defn -testing
[name func]
Expand All @@ -106,27 +105,45 @@
(ct/report reports))

(defmacro checking
"A macro intended to replace the testing macro in clojure.test with a
{:doc "A macro intended to replace the testing macro in clojure.test with a
generative form. To make (testing \"doubling\" (is (= (* 2 2) (+ 2 2))))
generative, you simply have to change it to
(checking \"doubling\" 100 [x gen/int] (is (= (* 2 x) (+ x x)))).
(checking \"doubling\" [x gen/int] (is (= (* 2 x) (+ x x)))).

You can optionally pass in a map options instead of the number of tests,
You can optionally pass the same options as test.check's defspec,
which will be passed to `clojure.test.check/quick-check`, e.g.:

(checking \"doubling\" {:num-tests 100 :seed 123 :max-size 10}
(checking \"doubling\" 100 ;; number
[x gen/int]
(is (= (* 2 x) (+ x x))))

(checking \"doubling\" {:num-tests 100 :seed 123 :max-size 10} ;; options map
[x gen/int]
(is (= (* 2 x) (+ x x))))

For background, see
http://blog.colinwilliams.name/blog/2015/01/26/alternative-clojure-dot-test-integration-with-test-dot-check/"
[name num-tests-or-options bindings & body]
`(-testing ~name
(fn []
(let [final-reports# (atom [])]
(qc-and-report-exception final-reports# ~num-tests-or-options ~bindings ~@body)
(doseq [r# @final-reports#]
(-report r#))))))
:arglists '([name bindings & body] [name num-tests-or-options bindings & body])}
[name & check-decl]
(let [[num-tests-or-options bindings body]
(cond
(and (or (number? (first check-decl))
(map? (first check-decl)))
(vector? (second check-decl)))
[(first check-decl) (second check-decl) (nnext check-decl)]

(vector? (first check-decl))
[nil (first check-decl) (next check-decl)]

:else (throw (#?(:clj IllegalArgumentException.
:cljs js/Error.) "Arguments to `checking` must be either [name bindings & body] or [name num-tests-or-options bindings & body]")))
num-tests-or-options (tc.clojure-test/process-options num-tests-or-options)]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fact that you're calling process-options with the form at macroexpansion-time means that users can't have a non-literal expression; e.g., I think (checking "something" (assoc some-defaults :num-tests 40) ...) would fail (you could add a test for this).

Now that I think about that distinction, I believe your check for number? and map? farther up are not sufficient, since it could also be a symbol or sequence. I think the cleanest thing would just be to only check (vector? (second check-decl)) there.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, but... (read the comment below)

`(-testing ~name
(fn []
(let [final-reports# (atom [])]
(qc-and-report-exception final-reports# ~num-tests-or-options ~bindings ~@body)
(doseq [r# @final-reports#]
(-report r#)))))))

(defmacro for-all
"An alternative to clojure.test.check.properties/for-all that uses
Expand Down
16 changes: 14 additions & 2 deletions test/com/gfredericks/test/chuck/clojure_test_test.cljc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(ns com.gfredericks.test.chuck.clojure-test-test
(:require #?(:clj [clojure.test :refer :all])
#?(:cljs [cljs.test :refer-macros [deftest is]])
#?(:cljs [cljs.test :refer-macros [deftest is testing]])
[clojure.test.check :refer [quick-check]]
[clojure.test.check.generators :as gen]
[com.gfredericks.test.chuck.clojure-test #?(:clj :refer :cljs :refer-macros) [checking for-all]]))
Expand All @@ -12,6 +12,13 @@
(is (< i 0))))

(deftest options-test
;; no option is OK, defaults to 100 tests
(let [nb-runs (atom 0)]
(checking "no option works" [i gen/s-pos-int]
(swap! nb-runs inc)
(is (pos? i)))
(testing "no option means 100 runs (test.check's default)"
(is (= 100 @nb-runs))))
;; empty map is OK, defaults to 100 tests
(checking "strings are strings" {} [s gen/string-ascii]
(is (string? s)))
Expand All @@ -23,7 +30,12 @@
(is (contains? #{-1 0 1} i)))
;; passes because of max-size
(checking "short strings" {:num-tests 100 :max-size 9} [s gen/string-ascii]
(is (< (count s) 10))))
(is (< (count s) 10)))
;; bad options throws
(testing "bad option throws"
(is (thrown? #?(:clj IllegalArgumentException :cljs js/Object)
(eval `(checking "numbers are numbers" "opts as string" [i gen/int]
(is (int? i))))))))

(deftest counter
(checking "increasing" 100 [i gen/s-pos-int]
Expand Down