diff --git a/README.md b/README.md index 4717b21..bf2ceac 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ This library addresses this issue by providing composable matcher combinators th ### `clojure.test` -Require the `matcher-combinators.test` namespace, which will extend `clojure.test`'s `is` macro to accept the `match?` and `thrown-match?` directives. +Refer `match?` and `thrown-match?` from the `matcher-combinators.test`: - `match?`: The first argument should be the matcher-combinator represented the expected value, and the second argument should be the expression being checked. - `thrown-match?`: The first argument should be a throwable subclass, the second a matcher-combinators, and the third the expression being checked. @@ -46,7 +46,7 @@ For example: ```clojure (require '[clojure.test :refer [deftest is]] - '[matcher-combinators.test] ;; adds support for `match?` and `thrown-match?` in `is` expressions + '[matcher-combinators.test :refer [match? thrown-match?]] '[matcher-combinators.matchers :as m]) (deftest test-matching-with-explicit-matchers @@ -100,7 +100,7 @@ For example: (is (match? {:name/first "Alfredo"} {:name/first "Alfredo" :name/last "da Rocha Viana" - :name/suffix "Jr."})))) + :name/suffix "Jr."}))) (deftest test-matching-nested-datastructures ;; Maps, sequences, and sets follow the same semantics whether at diff --git a/src/cljc/matcher_combinators/test.cljc b/src/cljc/matcher_combinators/test.cljc index 17c5bba..17cc875 100644 --- a/src/cljc/matcher_combinators/test.cljc +++ b/src/cljc/matcher_combinators/test.cljc @@ -4,8 +4,9 @@ This namespace provides useful placeholder vars for match?, match-with?, thrown-match? and match-roughly?; - the placeholders are nil (the actual implementations are extended - via the clojure.test/assert-expr multimethod), but importing these will prevent + the placeholders are macros that throw an error if used improperly + (the actual implementations are extended via the + clojure.test/assert-expr multimethod), but importing these will prevent linters from flagging otherwise undefined names. Even if not concerned about linting, it is necessary to have @@ -19,15 +20,90 @@ #?(:cljs [matcher-combinators.cljs-test] :clj [matcher-combinators.clj-test]))) -(declare ^{:arglists '([matcher actual])} - match?) -(declare ^{:arglists '([type->matcher matcher actual])} - match-with?) -(declare ^{:arglists '([matcher actual] - [exception-class matcher actual])} - thrown-match?) -(declare ^{:arglists '([delta matcher actual])} - match-roughly?) +(defn- bad-usage [expr-name] + `(throw (#?(:clj IllegalArgumentException. + :cljs js/Error.) + ~(str expr-name " must be used inside `is`.")))) + +(defmacro match? + "Check `actual` with the provided `matcher`. + + If `matcher` is a scalar or collection type except regex or map, uses the built-in matcher `equals`: + + * For scalars, `matcher` is compared directly with `actual`. + * For sequences, `matcher` specifies count and order of matching elements. The elements, themselves, are matched based on their types or predicates. + * For sets, `matcher` specifies count of matching elements. The elements, themselves, are matched based on their types or predicates. + + ```clojure + (is (match? 37 (+ 29 8))) + (is (match? \"this string\" (str \"this\" \" \" \"string\"))) + (is (match? :this/keyword (keyword \"this\" \"keyword\"))) + + (is (match? [1 3] [1 3])) + (is (match? [1 odd?] [1 3])) + (is (match? [#\"red\" #\"violet\"] [\"Roses are red\" \"Violets are ... violet\"])) + ;; use `m/prefix` when you only care about the first n items + (is (match? (m/prefix [odd? 3]) [1 3 5])) + ;; use `m/in-any-order` when order doesn't matter + (is (match? (m/in-any-order [odd? odd? even?]) [1 2 3])) + + (is (match? #{1 2 3} #{3 2 1})) + (is (match? #{odd? even?} #{1 2})) + ;; use `m/set-equals` to repeat predicates + (is (match? (m/set-equals [odd? odd? even?]) #{1 2 3})) + ``` + + If `matcher` is a regex, uses the built-in matcher `regex` (matches using `(re-find matcher actual)`): + + ```clojure + (is (match? #\"fox\" \"The quick brown fox jumps over the lazy dog\")) + ``` + + If `matcher` is a map, uses the built-in matcher `embeds` (matches when `actual` contains some of the same key/values as `matcher`): + + ```clojure + (is (match? {:name/first \"Alfredo\"} + {:name/first \"Alfredo\" + :name/last \"da Rocha Viana\" + :name/suffix \"Jr.\"})) + ``` + + Otherwise, `matcher` must be a matcher (implements the Matcher protocol)." + [matcher actual] + (bad-usage "match?")) + +(defmacro thrown-match? + "Asserts that evaluating `expr` throws an `exception-class`. + Also asserts that the exception data satisfies the provided `matcher`. + + Defaults to `clojure.lang.ExceptionInfo` if `exception-class` is not provided. + + ```clojure + (is (thrown-match? {:foo 1} + (throw (ex-info \"Boom!\" {:foo 1 :bar 2})))) + + (is (thrown-match? clojure.lang.ExceptionInfo + {:foo 1} + (throw (ex-info \"Boom!\" {:foo 1 :bar 2})))) + ```" + ([matcher expr] `(thrown-match? nil ~matcher ~expr)) + ([exception-class matcher expr] + (bad-usage "thrown-match?"))) + +(defmacro ^:deprecated match-with? + "DEPRECATED: `match-with?` is deprecated. Use `(match? (matchers/match-with matcher> ) )` instead." + [type->matcher matcher actual] + (bad-usage "match-with?")) + +(defmacro ^:deprecated match-equals? + "DEPRECATED: `match-equals?` is deprecated. Use `(match? (matchers/match-with [map? matchers/equals] ) )` instead." + [matcher actual] + (bad-usage "match-equals?")) + +(defmacro ^:deprecated match-roughly? + "DEPRECATED: `match-roughly?` is deprecated. Use `(match? (matchers/within-delta ) )` instead." + [delta matcher actual] + (bad-usage "match-roughly?")) #?(:clj (def build-match-assert diff --git a/test/clj/matcher_combinators/matchers_test.clj b/test/clj/matcher_combinators/matchers_test.clj index e628388..00b2177 100644 --- a/test/clj/matcher_combinators/matchers_test.clj +++ b/test/clj/matcher_combinators/matchers_test.clj @@ -8,7 +8,7 @@ [matcher-combinators.core :as c] [matcher-combinators.matchers :as m] [matcher-combinators.result :as result] - [matcher-combinators.test :refer [match?]] + [matcher-combinators.test :refer [match? thrown-match? match-with? match-roughly? match-equals?]] [matcher-combinators.test-helpers :as test-helpers :refer [abs-value-matcher]]) (:import [matcher_combinators.model Mismatch Missing InvalidMatcherType])) @@ -496,3 +496,25 @@ ::result/weight number?} (c/match {:payloads (m/pred pos? "positive numbers only please")} {:payloads -1}))))) + +(deftest bad-usage-test + (is (thrown? IllegalArgumentException + (match? :expected :actual))) + (is (thrown? IllegalArgumentException + (thrown-match? {:foo 1} + (throw (ex-info "bang!" {:foo 1}))))) + (is (thrown? IllegalArgumentException + (thrown-match? clojure.lang.ExceptionInfo + {:foo 1} + (throw (ex-info "bang!" {:foo 1}))))) + (is (thrown? IllegalArgumentException + (match-with? {java.lang.Long abs-value-matcher} + -5 + 5))) + (is (thrown? IllegalArgumentException + (match-equals? {:a 1} + {:a 1}))) + (is (thrown? IllegalArgumentException + (match-roughly? 0.1 + {:a 1 :b 3.0} + {:a 1 :b 3.05})))) diff --git a/test/cljs/matcher_combinators/cljs_example_test.cljs b/test/cljs/matcher_combinators/cljs_example_test.cljs index 5ff383f..bc7e3e2 100644 --- a/test/cljs/matcher_combinators/cljs_example_test.cljs +++ b/test/cljs/matcher_combinators/cljs_example_test.cljs @@ -7,7 +7,7 @@ [matcher-combinators.parser] [matcher-combinators.matchers :as m] [matcher-combinators.core :as c] - [matcher-combinators.test] + [matcher-combinators.test :refer-macros [match? thrown-match? match-with? match-equals? match-roughly?]] [matcher-combinators.test-helpers :as helpers]) (:import [goog.Uri])) @@ -55,6 +55,28 @@ (deftest passing-match (is (match? {:a 2} {:a 2 :b 1}))) +(deftest bad-usage-test + (is (thrown? js/Error + (match? :expected :actual))) + (is (thrown? js/Error + (thrown-match? {:foo 1} + (throw (ex-info "bang!" {:foo 1}))))) + (is (thrown? js/Error + (thrown-match? ExceptionInfo + {:foo 1} + (throw (ex-info "bang!" {:foo 1}))))) + (is (thrown? js/Error + (match-with? {js/number :stub} + -5 + 5))) + (is (thrown? js/Error + (match-equals? {:a 1} + {:a 1}))) + (is (thrown? js/Error + (match-roughly? 0.1 + {:a 1 :b 3.0} + {:a 1 :b 3.05})))) + (comment (deftest match?-no-actual-arg (testing "fails with nice message when you don't provide an `actual` arg to `match?`"