sinho provides a matcher called =* for writing tests with a high signal-to-noise ratio.
Works with Clojure, ClojureScript, Babashka, and ClojureDart.
See this notebook about Signal-To-Noise Ratio of Software Tests.
sinho is available from Clojars. Add the following dependency to your deps.edn or project.clj:
For Babashka, add to your bb.edn:
{:deps {me.lomin/sinho {:mvn/version "2.1.2"}}}
(ns me.lomin.sinho.example-test
(:require [clojure.test :refer :all]
[me.lomin.sinho.matcher :refer [=*]]))
(defn query-twitter-for [_user] ; fake implementation
{:name "Steven Collins"
:twitter "@lomin"
:following 169
:followers 122
:person-of-note {{:name "Borussia Dortmund"
:twitter "@BlackYellow"
:folowing 146
:folowers 4579000} :favorite
{:name "FC Bayern English"
:twitter "@FCBayernEN"
:following 49
:followers 11000000} :blocked}})
(deftest blocks-all-evil-test
(is (=* {:twitter "@lomin"
:person-of-note {{:twitter "@FCBayernEN"} :blocked}}
(query-twitter-for "@lomin"))))
(deftest failing-blocks-all-evil-test
(is (=* {:twitter "@lomin"
:person-of-note {{:instagram "@FCBayernEN"} :blocked}}
(query-twitter-for "@lomin"))))
(ns me.lomin.sinho.example-matcher-combinator-test
(:require
[clojure.test :as t :refer [deftest is]]
[me.lomin.sinho.matcher-combinator :refer [=*]]))
(defn query-twitter-for [_user] ; fake implementation
{:name "Steven Collins"
:twitter "@lomin"
:following 169
:followers 122
:person-of-note {{:name "Borussia Dortmund"
:twitter "@BlackYellow"
:folowing 146
:folowers 4579000} :favorite
{:name "FC Bayern English"
:twitter "@FCBayernEN"
:following 49
:followers 11000000} :blocked}})
(deftest blocks-all-evil-test-mc
(is (match? (=* {:twitter "@lomin"
:person-of-note {{:twitter "@FCBayernEN"} :blocked}})
(query-twitter-for "@lomin"))))
(deftest failing-blocks-all-evil-test-mc
(is (match? (=* {:twitter "@lomin"
:person-of-note {{:instagram "@FCBayernEN"} :blocked}})
(query-twitter-for "@lomin"))))
sinho's public API is five vars:
me.lomin.sinho.matcher/=*, the matcher;me.lomin.sinho.matcher/to-diff2, which turns a diff into deep-diff2's
records;me.lomin.sinho.matcher-combinator/=*, the matcher for match?;me.lomin.sinho.pred/pred and me.lomin.sinho.pred/named-pred, which wrap
a predicate for an expected position.Every other var and namespace is internal to sinho and may change in any release, even where it is public so that another sinho namespace can call it.
The =* matcher API is identical across Clojure, ClojureScript, Babashka, and ClojureDart. There are a few differences to be aware of:
=*, pred and the diff on the Dart VM, with the same
verdicts as the JVM and diffs that are = to the JVM's; a diff's collection
types and the order of a large map can differ, as "How a diff is built"
explains. (is (=* expected actual)) passes whatever
the diff, because ClojureDart's is cannot be extended; write
(is (= expected (=* expected actual))) instead. Records print as plain
maps, so #matcher_combinators.model.Mismatch{:expected 2, :actual 3} prints
as {:expected 2, :actual 3}. to-diff2 and the match? adapter
me.lomin.sinho.matcher-combinator are absent, because neither deep-diff2
nor matcher-combinators' core loads on ClojureDart. bb test:cljd runs
sinho's own tests there, with dart on the PATH.=* failures render as deep-diff2 diffs. On ClojureScript and Babashka, you get standard clojure.test failure output; me.lomin.sinho.matcher/to-diff2 turns a diff into deep-diff2's records, but without Kaocha integration.predNew in 3.0. Use (pred f) to match values by predicate instead of equality:
(require '[me.lomin.sinho.pred :refer [pred named-pred]])
;; Type check in a map
(=* {:name (pred string?)
:age (pred #(< 0 % 200))}
{:name "Alice" :age 30 :email "a@b.com"})
;; => match (returns expected)
;; Named predicate for readable failure messages
(=* (named-pred "positive?" pos?) -5)
;; => #Mismatch{:expected #Pred{:label "positive?"}, :actual -5}
Bare functions in expected positions are compared by value equality, not interpreted as predicates. This eliminates the ambiguity between "predicate-on-actual" and "literal function-value equality."
Wrap inline predicates with pred. If you previously used bare functions
in expected positions expecting predicate behavior, wrap them:
;; 2.x (ambiguous)
(=* string? actual)
;; 3.0 (unambiguous)
(=* (pred string?) actual)
Diff shapes may differ on ambiguous sets. If your tests pinned specific
diff shapes for sets of structurally-similar compound elements, the 3.0
greedy matching may produce a different (but still correct) diff. Update
those assertions or switch to verdict-level assertions ((= expected (=* ...))).
Stop using the specter navigator vars. sinho no longer depends on
specter, and the vars whose values were specter navigators, such as
me.lomin.sinho.diff/children-nav, are gone. The CHANGELOG lists them.
Use only the public API. The vars outside "Public API" are gone or
private, and diffs no longer carry :me.lomin.sinho.matcher/count-seq
metadata. The CHANGELOG lists the vars.
Performance is dramatically better. Map comparison is O(n) for atom
keys (down from O(n!) worst case). Set comparison is O(n^3) for verdicts
(down from factorial). Sequential comparison is O(m*n) (down from exponential).
=* finds the differences between expected and actual as a sequence of
paths, each a pair of a path into expected and a path into actual. It cannot
apply them one after another: a change can invalidate the paths that follow
it, as when a map key is replaced by its Missing record and a later path
still names the old key. So =* sorts the paths, merges them into one tree
in which paths share their common prefix, and applies the tree to expected
in a single pass. Each leaf replaces the value it reaches with a Mismatch,
Missing or Unexpected record built from the value its path reaches in
actual.
An insertion gives the collection type that specter 1.1.4 gave. Into a
vector on the JVM and Babashka, that is what subvec returns. On
ClojureScript a PersistentVector becomes a Subvec, and a Subvec
becomes a PersistentVector, through the rebuild sinho once added to
specter's InsertBeforeIndex, so successive insertions alternate between
the two. On ClojureDart, every insertion gives a PersistentVector. Into a
seq, an insertion at index 0 conses onto a list, a queue, a Cons or nil on
the JVM and Babashka, onto a list, a Cons or nil on ClojureDart, which has
no queue, and onto any seq on ClojureScript; every other insertion
concatenates and gives a LazySeq.
A diff is built from the caller's own values. A vector, map, set or record
that the diff changes in place keeps its kind and metadata, except a sorted
set or sorted map whose comparator cannot order a replaced element or key,
which becomes a hash set or hash map with the same metadata. A seq whose
element the diff changes becomes a LazySeq, and a collection an insertion
rebuilds gets the type above; neither keeps metadata. The diff itself
carries only :sinho/cost metadata. Replacing a key of a map of more than
eight entries goes through assoc, which turns an array map into a hash
map on every host but ClojureDart, which has no array map; the diff then
lists that map's entries in another order. Verdicts never depend on this
order.
Each dialect's suite runs through a Babashka task:
| Dialect | Command | Needs on the PATH |
|---|---|---|
| Clojure | bb test:clj | clojure |
| ClojureScript | bb test:node | clojure, node |
| Babashka | bb test:bb | bb 0.9.159 or later |
| ClojureDart | bb test:cljd | clojure, dart |
bb test runs the Clojure, ClojureScript and Babashka suites, and
bb test:browser serves the ClojureScript tests at http://localhost:9158/.
bb test:clj passes its arguments to Kaocha.
bb test:bb runs every *_test.cljc namespace under test/cljc.
bb test:cljd runs the same namespaces, except those in bb.edn's
cljd-unsupported-test-namespaces, whose dependencies do not load on
ClojureDart; name namespaces after it to run only those. It deletes test/cljd-out before compiling, because dart test runs
every compiled test file there, and output left from an earlier compile would
run as well.
Sinho (신호) means signal in Korean.
Can you improve this documentation? These fine people already did:
Steven Collins & lominEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |