{"id":16800290,"url":"https://github.com/camsaul/humane-are","last_synced_at":"2025-03-17T03:31:07.815Z","repository":{"id":58420152,"uuid":"531667499","full_name":"camsaul/humane-are","owner":"camsaul","description":"Drop-in replacement for clojure.test/are with better error output and better arg validation","archived":false,"fork":false,"pushed_at":"2022-09-02T03:17:10.000Z","size":165,"stargazers_count":31,"open_issues_count":2,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-05-01T21:38:29.681Z","etag":null,"topics":["cljs","cljs-test","clojure","clojure-test","clojurescript"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/camsaul.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null},"funding":{"github":"camsaul"}},"created_at":"2022-09-01T19:57:26.000Z","updated_at":"2024-03-05T04:10:57.000Z","dependencies_parsed_at":"2022-09-24T03:35:02.506Z","dependency_job_id":null,"html_url":"https://github.com/camsaul/humane-are","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/camsaul%2Fhumane-are","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/camsaul%2Fhumane-are/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/camsaul%2Fhumane-are/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/camsaul%2Fhumane-are/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/camsaul","download_url":"https://codeload.github.com/camsaul/humane-are/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243841203,"owners_count":20356443,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cljs","cljs-test","clojure","clojure-test","clojurescript"],"created_at":"2024-10-13T09:32:09.238Z","updated_at":"2025-03-17T03:31:07.512Z","avatar_url":"https://github.com/camsaul.png","language":"Clojure","funding_links":["https://github.com/sponsors/camsaul"],"categories":[],"sub_categories":[],"readme":"[![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/camsaul/humane-are/Tests/master?style=for-the-badge)](https://github.com/camsaul/humane-are/actions/workflows/tests.yml)\n[![License](https://img.shields.io/badge/license-Eclipse%20Public%20License-blue.svg?style=for-the-badge)](https://raw.githubusercontent.com/camsaul/humane-are/master/LICENSE)\n[![GitHub last commit](https://img.shields.io/github/last-commit/camsaul/humane-are?style=for-the-badge)](https://github.com/camsaul/humane-are/commits/)\n[![GitHub Sponsors](https://img.shields.io/github/sponsors/camsaul?style=for-the-badge)](https://github.com/sponsors/camsaul)\n[![cljdoc badge](https://img.shields.io/badge/dynamic/json?color=informational\u0026label=cljdoc\u0026query=results%5B%3F%28%40%5B%22artifact-id%22%5D%20%3D%3D%20%22humane-are%22%29%5D.version\u0026url=https%3A%2F%2Fcljdoc.org%2Fapi%2Fsearch%3Fq%3Dio.github.camsaul%2Fhumane-are\u0026style=for-the-badge)](https://cljdoc.org/d/io.github.camsaul/humane-are/CURRENT)\n\u003c!-- [![Codecov](https://img.shields.io/codecov/c/github/camsaul/humane-are?style=for-the-badge)](https://codecov.io/gh/camsaul/humane-are) --\u003e\n\u003c!-- [![Get help on Slack](http://img.shields.io/badge/slack-clojurians%20%23toucan-4A154B?logo=slack\u0026style=for-the-badge)](https://clojurians.slack.com/channels/toucan) --\u003e\n\u003c!-- [![Downloads](https://versions.deps.co/camsaul/humane-are/downloads.svg)](https://versions.deps.co/camsaul/humane-are) --\u003e\n\u003c!-- [![Dependencies Status](https://versions.deps.co/camsaul/humane-are/status.svg)](https://versions.deps.co/camsaul/humane-are) --\u003e\n\n[![Clojars Project](https://clojars.org/io.github.camsaul/humane-are/latest-version.svg)](https://clojars.org/io.github.camsaul/humane-are)\n\n# Humane Are\n\n```clj\n(require 'humane-are.core)\n\n(humane-are.core/install!)\n```\n\n`clojure.test/are` is great for writing lots of assertions quickly, but it has two big problems that prevent me from\nusing it everywhere:\n\n1. Failing assertions give no indication as to which set of arguments failed if you're using anything that pretty\nprints test output, such as [Humane Test Output](https://github.com/pjstadig/humane-test-output),\n[CIDER](https://github.com/clojure-emacs/cider), or [eftest](https://github.com/weavejester/eftest)\n2. `are` lets you shoot yourself in the foot by writing expressions that include `is` or `testing`, and wraps them in\n   another `is` without complaining\n\nHumane Are solves both of these problems.\n\n## Meaningful Error Messages in Failing Tests\n\nHere's a [real-world test using `are` that I wrote for\nMetabase](https://github.com/metabase/metabase/blob/bc4acbd2d1984e40ab91e87f61a40878939fb560/test/metabase/util_test.clj#L255-L274):\n\n```clj\n(deftest parse-currency-test\n  (are [s expected] (= expected\n                       (u/parse-currency s))\n    nil             nil\n    \"\"              nil\n    \"   \"           nil\n    \"$1,000\"        1000.0M\n    \"$1,000,000\"    1000000.0M\n    \"$1,000.00\"     1000.0M\n    \"€1.000\"        1000.0M\n    \"€1.000,00\"     1000.0M\n    \"€1.000.000,00\" 1000000.0M\n    \"-£127.54\"      -127.54M\n    \"-127,54 €\"     -127.54M\n    \"kr-127,54\"     -127.54M\n    \"€ 127,54-\"     -127.54M\n    \"¥200\"          200.0M\n    \"¥200.\"         200.0M\n    \"$.05\"          0.05M\n    \"0.05\"          0.05M))\n```\n\nWhat happens if there's a test failure? Here's the output with standard `are` with [Humane Test\nOutput](https://github.com/pjstadig/humane-test-output):\n\n```\nFail in parse-currency-test\n\nexpected: 1000.0M\n\n  actual: 1000.0\n    diff: - 1000.0M\n          + 1000.0\n```\n\nThere's no easy way to tell *which* specific assertion caused the test to fail.\n\n*Note: this isn't a problem if you're using normal \"inhumane\" test output, since you'd get something like*\n\n```\nFAIL in (parse-currency-test)\nexpected: (= 1000.0M (u/parse-currency \"$1,000.00\"))\n  actual: (not (= 1000.0M 1000.0))\n```\n\n*If that describes you, you can skip to the next section.*\n\nLet's try installing Humane Are, and running the test again:\n\n```clj\n(require 'humane-are.core)\n\n(humane-are.core/install!)\n```\n\n```\nFail in parse-currency-test\n(= 1000.0M (u/parse-currency \"$1,000.00\"))\n\nexpected: 1000.0M\n\n  actual: 1000.0\n    diff: - 1000.0M\n          + 1000.0\n```\n\nHumane Are adds `testing` context so you know which specific arguments caused the test to fail.\n\n## Anti-Foot-Shooting Protection\n\nHere's [another real-world\nexample](https://github.com/metabase/metabase/blob/bc4acbd2d1984e40ab91e87f61a40878939fb560/test/metabase/util_test.clj#L339-L346)\nthat I discovered just now while I was in the process of writing this README. What's wrong with this test?\n\n```clj\n(deftest email-\u003edomain-test\n  (are [domain email] (is (= domain\n                             (u/email-\u003edomain email))\n                          (format \"Domain of email address '%s'\" email))\n    nil              nil\n    \"metabase.com\"   \"cam@metabase.com\"\n    \"metabase.co.uk\" \"cam@metabase.co.uk\"\n    \"metabase.com\"   \"cam.saul+1@metabase.com\"))\n```\n\nLet's try changing one of the assertions, to try to make it fail. Here I've swapped out one of the domains from\n`metabase.com` to `metabase.commm`:\n\n```clj\n(deftest email-\u003edomain-test\n  (are [domain email] (is (= domain\n                             (u/email-\u003edomain email))\n                          (format \"Domain of email address '%s'\" email))\n    nil              nil\n    \"metabase.com\"   \"cam@metabase.commm\"\n    \"metabase.co.uk\" \"cam@metabase.co.uk\"\n    \"metabase.com\"   \"cam.saul+1@metabase.com\"))\n```\n\n```\n2 non-passing tests:\n\nFail in email-\u003edomain-test\nDomain of email address 'cam@metabase.commm'\nexpected: \"metabase.com\"\n\n  actual: \"metabase.commm\"\n    diff: - \"metabase.com\"\n          + \"metabase.commm\"\n\n\nFail in email-\u003edomain-test\n\nexpected: (is\n           (= \"metabase.com\" (u/email-\u003edomain \"cam@metabase.commm\"))\n           (format \"Domain of email address '%s'\" \"cam@metabase.commm\"))\n\n  actual: false\n```\n\nWhy are we getting *two* failures instead of *one*?!\n\nWe've unwittingly written a test that does two assertions instead of the one we thought we were getting: we're testing\nnot just\n\n```clj\n(is (= \"metabase.com\" (u/email-\u003edomain \"cam@metabase.commm\")))\n```\n\nbut\n\n```clj\n(is (is (= \"metabase.com\" (u/email-\u003edomain \"cam@metabase.commm\"))))\n```\n\nas well. `are` automatically wraps the assertions in its macroexpansion in `is`, so by including an `is` ourselves\nwe're actually getting `(is (is ...))`. This is generally the wrong thing to do. At best you're just doing an extra\nassertion everywhere, where one has borderline meaningless output when it fails; at worst you can wind up with\ntests that previously passed suddenly no longer passing in ways that are prone to make you pull your hair out.\n\nSuppose you've defined a `is` custom assertion method. If you tweak it so it stops returning a logically truthy value\nwhen the test passes, you can wind up with mystery test failures in places that use it:\n\n```clj\n(defmethod assert-expr 'broken=\n  [message [_ expected actual]]\n  `(let [expected# ~expected\n         actual#   ~actual]\n     (when-not (= expected# actual#)\n       (do-report {:type :fail, :message ~message, :expected expected#, :actual actual#}))))\n\n(deftest x-test\n  (are [x] (is (broken= x 100))\n    100))\n```\n\n```\nFail in x-test\n\nexpected: (is (broken= 100 100))\n\n  actual: nil\n```\n\nWe're accidentally testing both `(is (broken= 100 100))` and `(is (is (broken= 100 100))`, and while the former is\nfine, the latter fails because the macroexpansion for `broken=` returns `nil`.\n\nIt's better just to disallow `is` or `testing` forms inside `are` to prevent you from shooting yourself in the foot.\n\nHumane Are adds an `fdef` [spec](https://github.com/clojure/spec.alpha/) to `are` to validate the expression form\nduring macroexpansion; if the expression is a list starting with a symbol that would resolve to `clojure.test/is` or\n`clojure.test/testing` (or `cljs.test/` for ClojureScript) it will fail spec validation, triggering an error during\nmacroexpansion. Here's an example of the useful errors Humane Are gives you:\n\n```\nCall to clojure.test/are did not conform to spec.\n#:clojure.spec.alpha{:problems\n                     [{:path [:expr],\n                       :pred (clojure.core/complement humane-are.core/is-or-testing-form?),\n                       :val (is (= domain (u/email-\u003edomain email)) (format \"Domain of email address '%s'\" email)),\n                       :via [],\n                       :in [1]}]}\n```\n\n## How Does it Work?\n\n`(humane-are.core/install!)` simply swaps out the `clojure.core/are` macro with a replacement macro,\n`humane-are.core/are+`, and defines a spec for `are` with `clojure.spec/fdef`. Any time Clojure macroexpands an `are`\nform after installing it, it will use the new macro with extra `testing` context, and Clojure will check the args\nusing the spec. The replacement macro uses the same underlying namespace, `clojure.template`, that `clojure.test/are`\nuses, so the behavior is otherwise exactly the same.\n\nIf you don't want to *replace* `clojure.core/are`, you can use `humane-are.core/are+` directly without installing it.\n\nDon't be afraid to install it tho. If you change your mind or hate fun you can use `humane-are.core/uninstall!` to\nuninstall Humane Are and go back to a sad world of imhumane `are`.\n\nI've tried [living in a world of having a separate custom version of\n`are`](https://github.com/metabase/metabase/blob/bc4acbd2d1984e40ab91e87f61a40878939fb560/test/metabase/test.clj#L359-L378)\nfor a few years now and I think having tried it both ways replacing `clojure.core/are` is absolutely the way to go.\n\n## ClojureScript Support\n\n`humane-are.core/are+` works with ClojureScript, including both the extra testing context\n(`cljs.test/testing` in this case) and spec-based validation, without jumping thru any hoops.\n\n`cljs.test/are` and `humane-are/are+` are macros, which means they normally get macroexpanded in a JVM Clojure context\nbefore ClojureScript ever sees them. This means you can only `install!` Humane Are in a Clojure context. To `install!`\nHumane Are so it's used when compiling macros for ClojureScript, you can create a `.cljc` file like this:\n\n```clj\n(ns some-cljc-namespace\n  (:require [humane-are.core]))\n\n#?(:clj\n   (humane-are.core/install!))\n```\n\nAs a convenience this library provides the namespace `humane-are.install` which does exactly the same thing. Simply\nrequiring this namespace in a `.cljs` or `.cljc` file will install Humane Are for you.\n\n## License\n\nCode and documentation copyright © 2022 [Cam Saul](https://camsaul.com).\n\nDistributed under the [Eclipse Public License](https://raw.githubusercontent.com/camsaul/humane-are/master/LICENSE),\nsame as Clojure.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcamsaul%2Fhumane-are","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcamsaul%2Fhumane-are","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcamsaul%2Fhumane-are/lists"}