matcher-combinators

repository·master·Indexed 20 days ago

https://github.com/nubank/matcher-combinators

A Clojure library for making expressive, composable assertions about nested data structures. It allows verification of data shapes without requiring exact equality for every field, providing specialized matchers for sequences, sets, and maps (which use partial matching via `embeds` by default). It integrates with `clojure.test` via `match?` and `thrown-match?` and includes utilities like `m/via` for value transformation and `m/nested-equals` for strict deep equality.

Tokens
4.1K
Snippets
14
Records
18
Agent score
67%

What's inside matcher-combinators

  1. Use negative matchers with caution

    master

    Negative matchers assert the absence of something. They are available but generally discouraged because they can reduce code readability.

    • mismatch: A negation matcher that passes if the underlying matcher fails to match the actual value.
    • absent: Specifically for maps. Matches when the actual map is missing the key associated with the absent matcher.

    Best Practice: Use absent only when the absence of a key is behaviorally significant. For general presence checks, prefer using a positive matcher like any?.

    ;; Example: Using mismatch to assert absence in a list
    (is (match? (mismatch (embeds [odd?])) actual))
    
    ;; Example: Using absent for map key absence
    (is (match? {:a absent :b 1} {:b 1}))
  2. How scalar and structural values are matched

    master

    Matcher-combinators uses default interpretations for standard Clojure types to make assertions more readable:

    • Scalars: Most scalar values (numbers, strings, keywords) are interpreted as an equals matcher.
    • Regular Expressions: Regex literals are handled specially to perform regex matching rather than equality.
    • Sequences: Interpreted as an equals matcher, meaning they must match in both count and order. Elements within the sequence are matched based on their own types.
    • Sets: Interpreted as an equals matcher. Note that matching sets is an $O(n!)$ operation; avoid using this for large sets.
    • Maps: Interpreted as an embeds matcher. This means the matcher only checks if the specified keys exist and match; it ignores un-specified keys in the actual data.
    ;; Scalars (interpreted as equals)
    (is (match? 37 (+ 29 8)))
    (is (match? "this string" (str "this" " " "string")))
    
    ;; Regex
    (is (match? #"fox" "The quick brown fox jumps over the lazy dog"))
    
    ;; Sequences (order and count matter)
    (is (match? [1 3] [1 3]))
    
    ;; Maps (ignores extra keys)
    (is (match? {:name/first "Alfredo"}
                {:name/first  "Alfredo"
                 :name/last   "da Rocha Viana"}))
  3. Understand default matcher behavior

    master

    When you provide a value to match? without wrapping it in a specific matcher, the library applies a default interpretation based on the type:

    • Scalars and Collections (except regex and maps): Uses equals.
    • Regex: Uses regex.
    • Maps: Uses embeds (partial matching).

    You can use matcher-for to inspect which matcher will be applied to a specific value.

    (require '[matcher-combinators.matchers :as matchers])
    
    (matchers/matcher-for {:this :map})
    ;; => #function[matcher-combinators.matchers/embeds]
  4. Release a new version of matcher-combinators

    master

    To release a new version and deploy it to Clojars, follow these steps:

    1. Sync your local environment: Ensure your local master branch is synchronized with the remote GitHub repository.
    2. Update version metadata: Verify that CHANGELOG.md and version.edn contain the correct new version number. If they do not, update them, commit the changes, and push them to GitHub.
    3. Execute release: Run the release command using bb.

    Running the release command creates a git tag corresponding to the current version and pushes it to GitHub. This push triggers a GitHub Action that automatically runs tests and uploads the JAR files to Clojars.

    # 1. Sync master
    git checkout master
    git pull
    
    # 2. (Manual step) Update CHANGELOG.md and version.edn, then commit/push
    
    # 3. Run release
    bb release
  5. Transform actual values with the `via` matcher

    master

    The via matcher allows you to transform the actual value before it is matched against the expected structure. This is useful for matching serialized strings against parsed data or pre-processing results.

    When paired with match-with, you can apply transformations like sorting to the actual result before matching, which can serve as a performance-optimized alternative to in-any-order for sortable values.

    ;; Example: Using via to parse strings during matching
    (let [result {:payloads ["{:foo :bar :baz :qux}"]}]
      (is (match? {:payloads [(m/via read-string {:foo :bar})]} 
                  {:payloads result})))
    
    ;; Example: Using match-with + via to sort actual results before matching
    (testing "using `match-with` + `via` we can sort the actual result before matching"
      (is (match? (m/match-with
                   [vector? (fn [expected] (m/via sort expected))]
                   {:payloads [1 2 3]})
                  {:payloads (shuffle [3 2 1])}))) 
  6. Override default map matching with `nested-equals`

    master

    By default, maps use embeds (partial matching). If you need to perform strict, deep equality checks on maps, use the nested-equals matcher instead of manually wrapping every nested map with equals.

    ;; Strict deep equality using nested-equals
    (is (match? (m/nested-equals {:a {:b {:c odd?}}}))
        {:a {:b {:c 1}}}))
  7. Run development tasks with Babashka

    master

    This project uses bb (Babashka) for development tasks. Ensure you have Babashka installed.

    Start nREPL

    bb dev

    Run Tests

    Use the following commands to run specific test suites:

    • bb test:clj: Run only Clojure tests.
    • bb test:midje: Run only Midje tests.
    • bb test:node: Run only ClojureScript tests.
    • bb test:browser: Run ClojureScript tests in a browser at http://localhost:9158/.

    Linting and Formatting

    • bb lint: Check formatting and linting.
    • bb lint:fix: Automatically fix formatting and linting issues.
  8. Integrate matcher-combinators with clojure.test

    master

    To use matcher-combinators within clojure.test, require the matcher-combinators.test namespace. This extends the is macro to support two new directives:

    • match?: Compares an expected matcher-combinator against an actual expression.
      • Syntax: (is (match? <matcher> <expression>))
    • thrown-match?: Asserts that an expression throws an exception and that the exception's data matches a specific matcher.
      • 3-arity syntax: (is (thrown-match? <exception-class> <matcher> <expression>))
      • 2-arity syntax: (is (thrown-match? <matcher> <expression>)) (matches against the exception's data).

    When a match fails, the library provides pretty-printed diffs to help identify the discrepancy.

    ```clojure
    (require '[clojure.test :refer [deftest is]]
             '[matcher-combinators.test] ;; adds support for `match?` and `thrown-match?` in `is` expressions
             '[matcher-combinators.matchers :as m])
    
    (deftest test-matching-with-explicit-matchers
      (is (match? (m/equals 37) (+ 29 8)))
      (is (match? (m/regex #
  9. Use matcher-combinators with clojure.test

    master

    To use matcher-combinators in your tests, require matcher-combinators.test. This adds support for match? and thrown-match? within standard clojure.test expressions like is.

    Commonly, you will also require matcher-combinators.matchers (often aliased as m) to use explicit matchers.

    (require '[clojure.test :refer [deftest is]]
             '[matcher-combinators.test] 
             '[matcher-combinators.matchers :as m])
    
    (is (match? (m/equals 37) (+ 29 8)))
  10. Match sequences and collections

    master

    Sequences are interpreted as equals matchers, meaning they check for both the correct count and the correct order of elements. Elements within the sequence are matched based on their types or provided predicates.

    Key Sequence Matchers

    • Standard Sequence: Matches exact order and count. Elements can be predicates (e.g., odd?).
    • m/prefix: Matches only the first n items of a sequence.
    • m/in-any-order: Matches elements regardless of their position in the sequence.

    Set Matching

    Sets are interpreted as equals matchers. To match sets where predicates might repeat (e.g., checking if a set contains two odd numbers), use m/set-equals.

    Map Matching

    Maps are interpreted as embeds matchers. This means the matcher only cares about the keys specified; any extra keys present in the target map are ignored.

    ;; Sequence matching
    (is (match? [1 odd?] [1 3]))
    (is (match? (m/prefix [odd? 3]) [1 3 5]))
    (is (match? (m/in-any-order [odd? odd? even?]) [1 2 3]))
    
    ;; Set matching
    (is (match? #{odd? even?} #{1 2}))
    (is (match? (m/set-equals [odd? odd? even?]) #{1 2 3}))
    
    ;; Map matching (ignores extra keys)
    (is (match? {:name/first "Alfredo"}
                {:name/first "Alfredo" :name/last "da Rocha"}))
  11. Transform values during matching with `m/via`

    master

    The m/via combinator allows you to apply a transformation function (like read-string) to the actual value before it is matched against the expected value. This is useful when dealing with serialized data or strings that represent Clojure data structures.

    ;; Applies read-string to the actual value before matching
    (is (match? {:payloads [(m/via read-string {:foo :bar})]}
                {:payloads ["{:foo :bar}"]}))