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

Change stubs to throw exceptions if used incorrectly #207

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
98 changes: 87 additions & 11 deletions src/cljc/matcher_combinators/test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <type->matcher> <expected>) <actual>)` 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] <expected>) <actual>)` instead."
[matcher actual]
(bad-usage "match-equals?"))

(defmacro ^:deprecated match-roughly?
"DEPRECATED: `match-roughly?` is deprecated. Use `(match? (matchers/within-delta <expected>) <actual>)` instead."
[delta matcher actual]
(bad-usage "match-roughly?"))

#?(:clj
(def build-match-assert
Expand Down
24 changes: 23 additions & 1 deletion test/clj/matcher_combinators/matchers_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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]))

Expand Down Expand Up @@ -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}))))
24 changes: 23 additions & 1 deletion test/cljs/matcher_combinators/cljs_example_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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]))

Expand Down Expand Up @@ -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?`"
Expand Down