{"id":29095423,"url":"https://github.com/alexanderjamesking/spy","last_synced_at":"2025-12-12T01:32:58.958Z","repository":{"id":25779346,"uuid":"106097293","full_name":"alexanderjamesking/spy","owner":"alexanderjamesking","description":"Clojure/ClojureScript library for stubs, spies and mocks.","archived":false,"fork":false,"pushed_at":"2025-06-24T09:19:20.000Z","size":949,"stargazers_count":178,"open_issues_count":1,"forks_count":9,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-06-28T05:03:31.809Z","etag":null,"topics":["clojure","clojurescript","mock","mocking","spy","stub","stubbing","testing"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alexanderjamesking.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-10-07T12:46:04.000Z","updated_at":"2025-06-24T09:19:22.000Z","dependencies_parsed_at":"2023-02-13T00:30:58.700Z","dependency_job_id":"b0b7b468-4ed8-4237-b051-f07c1421ae1d","html_url":"https://github.com/alexanderjamesking/spy","commit_stats":{"total_commits":232,"total_committers":4,"mean_commits":58.0,"dds":0.04741379310344829,"last_synced_commit":"42dd12e90d175c7867bd985f84cf3fddead6ab5a"},"previous_names":["alexanderjamesking/clj-spy"],"tags_count":33,"template":false,"template_full_name":null,"purl":"pkg:github/alexanderjamesking/spy","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexanderjamesking%2Fspy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexanderjamesking%2Fspy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexanderjamesking%2Fspy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexanderjamesking%2Fspy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alexanderjamesking","download_url":"https://codeload.github.com/alexanderjamesking/spy/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexanderjamesking%2Fspy/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262376658,"owners_count":23301383,"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":["clojure","clojurescript","mock","mocking","spy","stub","stubbing","testing"],"created_at":"2025-06-28T11:01:22.938Z","updated_at":"2025-12-12T01:32:58.950Z","avatar_url":"https://github.com/alexanderjamesking.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"docs/logo.svg\" width=\"266\"\u003e\n\n[![Clojars Project](https://img.shields.io/clojars/v/tortue/spy.svg)](https://clojars.org/tortue/spy)\n\n# Spy\n\nSpy - a Clojure and ClojureScript library for stubs, spies and mocks. This library is aimed at users of [clojure.test](https://clojure.github.io/clojure/clojure.test-api.html).\n\nIt records calls and responses to and from a function, allowing you to verify interactions. Terms used in this library are as follows, there are many different names for Mocks, see [Test Doubles, Fakes, Mocks and Stubs](https://blog.pragmatists.com/test-doubles-fakes-mocks-and-stubs-1a7491dfa3da) for more detail.\n\n1. Stub - function that returns a hardcoded value\n2. Spy - wrapper around a function allowing verification of interactions with the function\n3. Mock  - function with a fake implementation to be used in place of the real thing\n\n## Usage\n\n### REPL (Clojure)\n\n```clojure\n(require '[spy.core :as spy]       ;; the core library with functions returning booleans\n         '[spy.assert :as assert]  ;; assertions wrapping clojure.test/is\n         '[spy.test]               ;; assert-expr definitions for clojure.test\n         '[clojure.test :refer [testing is]])\n\n(defn adder [x y] (+ x y))\n\n(def spy-adder (spy/spy adder))\n\n(is (= [] (spy/calls spy-adder)))\n(is (= [] (spy/responses spy-adder)))\n\n(is (true? (spy/not-called? spy-adder))) ;; spy.core/not-called? returns a boolean\n\n(assert/not-called? spy-adder) ;; spy.assert/not-called? returns a boolean, but also wraps clojure.test/is so failures are reported\n\n(testing \"Let's see what a failure looks like...\"\n  (assert/called? spy-adder))\n\n;; FAIL in () (form-init4641634702245604141.clj:37)\n;; Let's see what a failure looks like...\n;; Expected at least 1 call\n;; Actual: 0 calls.\n;; expected: (spy.core/called-at-least-n-times? spy-adder 1)\n;;   actual: (not (spy.core/called-at-least-n-times? #function[clojure.lang.AFunction/1] 1))\n;; false\n\n\n(testing \"calling the function\"\n  (is (= 3 (spy-adder 1 2))))\n\n(testing \"calls to the spy can be accessed via spy/calls\"\n  (is (= [[1 2]] (spy/calls spy-adder))))\n\n(testing \"responses from the spy can be accessed via spy/responses\"\n  (is (= [3] (spy/responses spy-adder))))\n\n(testing \"let's do another call\"\n  (is (= 42 (spy-adder 40 2))))\n\n(testing \"calls and responses are stored on the spy using metadata\"\n  (meta spy-adder) ;; {:calls     #atom[[(1 2)] 0x7612740d],\n                   ;;  :responses #atom[[3] 0x26525904]}\n  (let [{:keys [calls responses]} (meta spy-adder)]\n    (is (= [[1 2] [40 2]] @calls))\n    (is (= [3 42] @responses))))\n\n(testing \"but they can be access via spy/calls and spy/responses\"\n  (is (= [[1 2] [40 2]] (spy/calls spy-adder)))\n  (is (= [3 42] (spy/responses spy-adder))))\n\n(testing \"we can check if the spy was called with some arguments\"\n  (is (true? (spy/called-with? spy-adder 1 2)))\n  (is (false? (spy/called-with? spy-adder 1 59))))\n\n(testing \"but spy.assert gives us better error messages when our assertions don't hold true\"\n  (assert/called-with? spy-adder 66 99))\n\n(testing \"spy defines assert-expr for core spy verification\"\n  (is (spy/called? spy-adder))\n  (is (spy/called-with? spy-adder 66 99)))\n\n;; FAIL in () (form-init15061478131364358.clj:197)\n;; assert gives us better error messages when our assertions don't hold true\n;; Expected a call with (66 99)\n;; Actual calls: [(1 2) (40 2)]\n;; expected: (spy.core/called-with? spy-adder 66 99)\n;;   actual: (not (spy.core/called-with? #function[clojure.lang.AFunction/1] 66 99))\n;;false\n\n```\n\n### Spies\n\n```spy.core/spy``` wraps a function and records calls to the function and responses returned by the function, this is done using an [atom](https://clojuredocs.org/clojure.core/atom). Calls and responses are stored on the function itself using [metadata](https://clojure.org/reference/metadata).\n\n```clojure\n(defn my-adder [x y]\n  (+ x y))\n\n(let [f (spy/spy my-adder)] ;; create a spy that wraps a simple adder function\n      (is (spy/not-called? f)) ;; verify it hasn't been called yet\n      (is (= 3 (f 1 2))) ;; call the function\n      (is (spy/called-with? f 1 2)) ;; verify it was called with the arguments\n      (is (spy/called-once? f))) ;; verify it was called only once\n```\n\n### Stubs\n\nA stub is a spy that wraps [constantly](https://clojuredocs.org/clojure.core/constantly), providing us with a function that returns a value and giving us the ability to verify calls were made to the stub.\n\n```clojure\n(let [f (spy/stub 42)] ;; create a stub that returns a hardcoded value\n      (is (spy/not-called? f)) ;; verify the stub has not been called yet\n      (f) ;; call the stub\n      (is (spy/called? f))\n      (is (spy/called-once? f))\n      (f) ;; call it for a second time\n      (f) ;; call if for a third time\n      (is (spy/called-n-times? f 3))) ;; verify it was called 3 times\n```\n\n### Mocks  (also known as Fakes / Test Doubles)\n\nTo implement a mock you just need to implement a function that has the same contract as the one you're replacing, the best person to do this is you! For convenience this library provides ```s/mock``` which is an alias for ```s/spy```, it's up to you to write the function that mocks the behaviour:\n\n```clojure\n(let [f (spy/mock (fn [x] (if (= 1 x)\n                            :one\n                            :something-else)))]\n      (is (= :one (f 1)))\n      (is (spy/called-once? f))\n      (is (= :something-else (f 42))))\n```\n\n### Custom Call Matching\n\nThe `call-matching?` function allows you to write custom predicates to verify that your spy was called with arguments that match specific conditions.\n\n```clojure\n(let [f (spy/spy (fn [x y] (+ x y)))]\n  (f 42 88)\n  ;; Check if any call had 42 as the first argument\n  (is (spy/call-matching? f (fn [call-args]\n                              (= 42 (first call-args)))))\n\n  ;; You can also match against maps\n  (let [f2 (spy/spy)]\n    (f2 {:command \"hello\" :value 42})\n    (is (spy/call-matching? f2 (fn [args]\n                                 (= \"hello\" (:command (first args))))))))\n```\n\n### Exceptions\n\nIf you spy on a function that throws an exception then Spy will catch your exception, record it in the responses, then re-throw the original exception, thus enabling you to test that the exception was thrown. A ```stub-throws``` helper function is provided.\n\n#### Clojure\n```clojure\n(let [f (spy/stub-throws (Exception. \"Goodbye World!\"))]\n      (is (thrown? Exception (f)))\n      (is (= 1 (count (spy/responses f))))\n      (is (contains? (spy/first-response f) :thrown))\n      (is (= \"Goodbye World!\" (-\u003e (spy/first-response f) :thrown :cause))))\n```\n\n#### ClojureScript\n```clojure\n(let [f (spy/stub-throws (js/Error \"Goodbye World!\"))]\n      (is (thrown? js/Object (f)))\n      (is (= 1 (count (spy/responses f))))\n      (is (contains? (spy/first-response f) :thrown)))\n```\n\n### Using with-redefs to replace functions with spies\n\nIf you are testing synchronous code then you can replace functions using [with-redefs](https://clojuredocs.org/clojure.core/with-redefs), if you're testing async code then it's safer to pass the functions in using dependency injection, I don't recommend using ~with-redefs~.\n\n```clojure\n(ns spy-example.core-test\n  (:require [clojure.test :refer [deftest testing is]]\n            [spy.core :as spy]))\n\n(def beatle-\u003eemail\n  {:john   \"john.lennon@beatles.com\"\n   :paul   \"paul.mccartney@beatles.com\"\n   :george \"george.harrison@beatles.com\"\n   :ringo  \"ringo.starr@beatles.com\"})\n\n(defn lookup-email [beatle-id]\n  (get beatle-\u003eemail beatle-id))\n\n(defn send-message [email message]\n  (println (str \"Sending \" message \" to \" email))\n  nil)\n\n(defn email-beatle [beatle-id message]\n  (when-let [email (lookup-email beatle-id)]\n    (send-message email message)))\n\n(deftest email-beatle-test\n  (testing \"A message is sent to a Beatle\"\n    ;; example 1 - wrap the original fn (so it is still called)\n    (with-redefs [send-message (spy/spy send-message)]\n      (email-beatle :ringo \"Hello Ringo!\")\n      (is (spy/called-once? send-message))\n      (is (spy/called-with? send-message \"ringo.starr@beatles.com\" \"Hello Ringo!\"))))\n\n  (testing \"A message is not sent to a Rolling Stone\"\n    ;; example 2 - call spy without passing a fn (to avoid sending the email)\n    (with-redefs [send-message (spy/spy)]\n      (email-beatle :mick \"Hello Mr Jagger!\")\n      (is (spy/not-called? send-message)))))\n```\n\n### Protocols\n\nCurrently, only Clojure is supported. I'm open to suggestions on how to support ClojureScript, contributions are welcome.\n\n`spy.protocol/mock` uses the same signature as `reify` and can be used\nto mock multiple protocols.\n\n``` clojure\n(require '[spy.protocol :as protocol])\n\n(defprotocol Rockable\n  (rock [this x] \"rock this!\"))\n\n(defprotocol Tuneable\n  (tune [this x y]))\n\n(def my-mock\n  (protocol/mock\n    Rockable\n    (rock [_ x]\n      (str \"Rock: \" x \"!\"))\n\n    Tuneable\n    (tune [_ x y]\n      (str \"Tune: \" x \", \" y \"!\"))))\n\n;; `protocol/spies` returns a map of the spy functions on the mock\n(protocol/spies my-mock)\n;; {:rock #function[clojure.lang.AFunction/1],\n;;  :tune #function[clojure.lang.AFunction/1]}\n\n;; use the core functions from the spy ns on the protocol spy\n(spy/call-count (:rock (protocol/spies my-mock)))\n;; 0\n\n;; call the rock method\n(rock my-mock \"Living easy, lovin' free\")\n\n;; extract the spy from the mock and verify\n(spy/called-with? (:rock (protocol/spies my-mock))\n                  my-mock\n                  \"Living easy, lovin' free\")\n;; true\n```\n\nYou will also find a `spy` macro within the protocol namespace, this\ncan also be used to spy on multiple protocols: `(spy.protocol/spy Proto1...ProtoN impl)`\n\n## Contributing\n\nPull requests are welcome. Please run the test suite and check that all tests pass prior to submission.\n\nTests:\n```\n$ lein test\n```\n\nCode coverage:\n```\n$ lein cloverage\n```\n\n## License\n```\nMIT License\n\nCopyright (c) 2025 Alexander James King\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexanderjamesking%2Fspy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexanderjamesking%2Fspy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexanderjamesking%2Fspy/lists"}