├── docs ├── index.md ├── api │ └── index.md ├── custom.css ├── dev-guide.md ├── quickstart.md └── user-guide.md ├── package.json ├── test └── unit │ ├── fixtures │ ├── protocols.cljc │ ├── functions.cljc │ ├── interop │ │ └── InterfaceFixture.java │ └── macros.cljc │ ├── optional_fake.cljc │ ├── fake.cljc │ ├── spy.cljc │ ├── was_called.cljc │ ├── was_called_once.cljc │ ├── was_matched_once.cljc │ ├── method_was_called.cljc │ ├── method_was_called_once.cljc │ ├── method_was_matched_once.cljc │ ├── was_not_called.cljc │ ├── reporter.clj │ ├── cyclically.cljc │ ├── method_was_not_called.cljc │ ├── original_val.cljc │ ├── runner.cljs │ ├── args_matcher.cljc │ ├── utils.cljc │ ├── was_called_fn_contract.cljc │ ├── method_was_called_fn_contract.cljc │ ├── methods_were_called_in_order.cljc │ ├── unpatch.cljc │ ├── context.cljc │ ├── unused_fakes_self_test.cljc │ ├── unchecked_fakes_self_test.cljc │ ├── recorded_fake.cljc │ ├── fake_fn_contract.cljc │ ├── were_called_in_order.cljc │ ├── positions.cljc │ ├── patch.cljc │ ├── reify_nice_fake.cljc │ └── reify_fake.cljc ├── .gitignore ├── mkdocs.yml ├── src └── clj_fakes │ ├── macro.clj │ ├── reflection.clj │ ├── core.cljc │ └── context.cljc ├── yarn.lock ├── LICENSE ├── tasks.cljs ├── tasks └── core.cljc ├── project.clj ├── CHANGELOG.md └── README.md /docs/index.md: -------------------------------------------------------------------------------- 1 | It is a placeholder file, it will be replaced by README.md during site generation. -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | It is a placeholder file, 2 | it will be replaced by API Reference docs generated by external tool. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tasks", 3 | "version": "1.0.0", 4 | "description": "Specifies deps needed for task execution.", 5 | "repository": "-", 6 | "license": "MIT", 7 | "dependencies": { 8 | "fs-extra": "^1.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/unit/fixtures/protocols.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.fixtures.protocols) 2 | 3 | (defprotocol AnimalProtocol 4 | (speak [this] [this name] [this name1 name2]) 5 | (eat [this food drink]) 6 | (sleep [this])) 7 | 8 | (defprotocol FileProtocol 9 | (save [this]) 10 | (scan [this])) -------------------------------------------------------------------------------- /docs/custom.css: -------------------------------------------------------------------------------- 1 | /* highlight subsections in TOC */ 2 | .bs-sidenav > .second-level { 3 | margin-left: 1em; 4 | } 5 | 6 | .bs-sidenav > .third-level { 7 | margin-left: 2em; 8 | } 9 | 10 | /* add scrolling to TOC */ 11 | .bs-sidebar.well { 12 | padding: 0; 13 | max-height: 88vh; 14 | overflow-y: auto; 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | *jar 4 | /lib/ 5 | /classes/ 6 | /target/ 7 | /checkouts/ 8 | .lein-deps-sum 9 | .lein-repl-history 10 | .lein-plugins/ 11 | .lein-failures 12 | .nrepl-port 13 | /clj_fakes.iml 14 | /.idea 15 | /site 16 | /resources 17 | /out 18 | /clj-fakes.iml 19 | .DS_Store 20 | /node_modules/ 21 | /profiles.clj 22 | -------------------------------------------------------------------------------- /test/unit/optional_fake.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.optional-fake 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [unit.fake-fn-contract :as c] 6 | [clj-fakes.core :as f] 7 | [clj-fakes.context :as fc])) 8 | 9 | (u/deftest+ 10 | "fake contract" 11 | (c/test-fake-fn-contract f/optional-fake fc/optional-fake false)) -------------------------------------------------------------------------------- /test/unit/fixtures/functions.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.fixtures.functions) 2 | 3 | (defn sum [x y] 4 | (+ x y)) 5 | 6 | (defn variadic 7 | ([] "[]") 8 | ([_] "[a]") 9 | ([_ _] "[a b]") 10 | ([_ _ & _] "[a b & c]")) 11 | 12 | (defmulti fib int) 13 | (defmethod fib 0 [_] 1) 14 | (defmethod fib 1 [_] 1) 15 | (defmethod fib :default [n] (+ (fib (- n 2)) (fib (- n 1)))) -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: clj-fakes 2 | site_description: Project documentation for clj-fakes framework. 3 | 4 | repo_url: https://github.com/metametadata/clj-fakes/ 5 | 6 | theme: cinder 7 | 8 | pages: 9 | - Readme: 'index.md' 10 | - Quickstart: 'quickstart.md' 11 | - User Guide: 'user-guide.md' 12 | - API Reference: 'api/index.md' 13 | - Developer Guide: 'dev-guide.md' 14 | -------------------------------------------------------------------------------- /test/unit/fake.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.fake 2 | (:require 3 | [unit.fake-fn-contract :as c] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [clj-fakes.context :as fc])) 7 | 8 | (u/deftest+ 9 | "fake contract" 10 | (c/test-fake-fn-contract 11 | ; we can't pass a macro into a function so let's wrap it into a func 12 | (fn [config] (f/fake config)) 13 | (fn [ctx config] (fc/fake ctx config)) 14 | true)) -------------------------------------------------------------------------------- /test/unit/spy.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.spy 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [unit.fixtures.functions :as funcs])) 7 | 8 | (u/deftest+ 9 | "function can be spied on" 10 | (f/with-fakes 11 | (f/patch! #'funcs/sum 12 | (f/recorded-fake [f/any funcs/sum])) 13 | 14 | (is (= 3 (funcs/sum 1 2))) 15 | (is (= 7 (funcs/sum 3 4))) 16 | (is (f/was-called funcs/sum [1 2])) 17 | (is (f/was-called funcs/sum [3 4])))) -------------------------------------------------------------------------------- /test/unit/was_called.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.was-called 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [unit.was-called-fn-contract :as c])) 7 | 8 | (u/deftest+ 9 | "contract" 10 | (c/test-was-called-fn-contract f/was-called 11 | #"^Function was not called the expected number of times\. Expected: > 0\. Actual: 0\.")) 12 | 13 | (u/deftest+ 14 | "passes if function was called several times" 15 | (f/with-fakes 16 | (let [foo (f/recorded-fake)] 17 | (foo) 18 | (foo 2) 19 | (foo 3) 20 | (is (f/was-called foo [f/any])) 21 | (is (f/was-called foo [])) 22 | (is (f/was-called foo [3]))))) -------------------------------------------------------------------------------- /test/unit/was_called_once.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.was-called-once 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [unit.was-called-fn-contract :as c])) 7 | 8 | (u/deftest+ 9 | "contract" 10 | (c/test-was-called-fn-contract f/was-called-once 11 | #"^Function was not called the expected number of times\. Expected: 1\. Actual: 0\.")) 12 | 13 | (u/deftest+ 14 | "throws if function was called more than once" 15 | (f/with-fakes 16 | (let [foo (f/recorded-fake)] 17 | (foo) 18 | (foo 2) 19 | (u/is-error-thrown 20 | #"^Function was not called the expected number of times\. Expected: 1\. Actual: 2\." 21 | (f/was-called-once foo [2]))))) -------------------------------------------------------------------------------- /src/clj_fakes/macro.clj: -------------------------------------------------------------------------------- 1 | (ns clj-fakes.macro 2 | (:require [clojure.pprint])) 3 | 4 | (defn -cljs-env? 5 | "Take the &env from a macro, and tell whether we are expanding into cljs. 6 | Source: https://groups.google.com/d/msg/clojurescript/iBY5HaQda4A/w1lAQi9_AwsJ" 7 | [env] 8 | (boolean (:ns env))) 9 | 10 | (defn P 11 | "Prints to the compiler's console at compile time. Sprinkle as needed within 12 | macros and their supporting functions to facilitate debugging during 13 | development. 14 | Inspired by: https://gist.github.com/michaelsbradleyjr/7509505" 15 | [& args] 16 | (binding [*out* *err*] 17 | (apply println args))) 18 | 19 | (defn PP 20 | "The same as P but uses pprint." 21 | [& args] 22 | (binding [*out* *err*] 23 | (apply clojure.pprint/pprint args))) -------------------------------------------------------------------------------- /test/unit/was_matched_once.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.was-matched-once 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [unit.was-called-fn-contract :as c])) 7 | 8 | (u/deftest+ 9 | "contract" 10 | (c/test-was-called-fn-contract f/was-matched-once 11 | #"^Function was not called the expected number of times\. Expected: > 0\. Actual: 0\.")) 12 | 13 | (u/deftest+ 14 | "throws if more than one call matches" 15 | (f/with-fakes 16 | (let [foo (f/recorded-fake [f/any nil])] 17 | (foo) 18 | (foo 2) 19 | (foo 3) 20 | (foo 2) 21 | (u/is-error-thrown 22 | #"^More than one call satisfies the provided args matcher\.\nArgs matcher: \[2\]\.\nMatched calls:\n\[\{:args \(2\), :return-value nil\} \{:args \(2\), :return-value nil\}\]" 23 | (f/was-matched-once foo [2]))))) -------------------------------------------------------------------------------- /test/unit/method_was_called.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.method-was-called 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [unit.method-was-called-fn-contract :as c] 7 | [unit.fixtures.protocols :as p])) 8 | 9 | (u/deftest+ 10 | "contract" 11 | (c/test-method-was-called-fn-contract f/method-was-called 12 | #"^Function was not called the expected number of times\. Expected: > 0\. Actual: 0\.")) 13 | 14 | (u/deftest+ 15 | "passes if function was called several times" 16 | (f/with-fakes 17 | (let [cow (f/reify-fake p/AnimalProtocol 18 | (speak :recorded-fake))] 19 | (p/speak cow) 20 | (p/speak cow 2) 21 | (p/speak cow 3) 22 | 23 | (is (f/method-was-called p/speak cow [2])) 24 | (is (f/method-was-called p/speak cow [])) 25 | (is (f/method-was-called p/speak cow [3]))))) -------------------------------------------------------------------------------- /test/unit/method_was_called_once.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.method-was-called-once 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [unit.method-was-called-fn-contract :as c] 7 | [unit.fixtures.protocols :as p])) 8 | 9 | (u/deftest+ 10 | "contract" 11 | (c/test-method-was-called-fn-contract f/method-was-called-once 12 | #"^Function was not called the expected number of times\. Expected: 1\. Actual: 0\.")) 13 | 14 | (u/deftest+ 15 | "throws if function was called more than once" 16 | (f/with-fakes 17 | (let [cow (f/reify-fake p/AnimalProtocol 18 | (speak :recorded-fake))] 19 | (p/speak cow) 20 | (p/speak cow 2) 21 | (u/is-error-thrown 22 | #"^Function was not called the expected number of times\. Expected: 1\. Actual: 2\." 23 | (f/method-was-called-once p/speak cow [2]))))) -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | fs-extra@^1.0.0: 6 | version "1.0.0" 7 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" 8 | dependencies: 9 | graceful-fs "^4.1.2" 10 | jsonfile "^2.1.0" 11 | klaw "^1.0.0" 12 | 13 | graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: 14 | version "4.1.11" 15 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 16 | 17 | jsonfile@^2.1.0: 18 | version "2.4.0" 19 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" 20 | optionalDependencies: 21 | graceful-fs "^4.1.6" 22 | 23 | klaw@^1.0.0: 24 | version "1.3.1" 25 | resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" 26 | optionalDependencies: 27 | graceful-fs "^4.1.9" 28 | -------------------------------------------------------------------------------- /test/unit/method_was_matched_once.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.method-was-matched-once 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [unit.method-was-called-fn-contract :as c] 7 | [unit.fixtures.protocols :as p])) 8 | 9 | (u/deftest+ 10 | "contract" 11 | (c/test-method-was-called-fn-contract f/method-was-matched-once 12 | #"^Function was not called the expected number of times\. Expected: > 0\. Actual: 0\.")) 13 | 14 | (u/deftest+ 15 | "throws if more than one call matches" 16 | (f/with-fakes 17 | (let [cow (f/reify-fake p/AnimalProtocol 18 | (speak :recorded-fake [f/any nil]))] 19 | (p/speak cow) 20 | (p/speak cow 2) 21 | (p/speak cow 3) 22 | (p/speak cow 2) 23 | (u/is-error-thrown 24 | #"^More than one call satisfies the provided args matcher\.\nArgs matcher: \[2\]\.\nMatched calls:\n.*" 25 | (f/method-was-matched-once p/speak cow [2]))))) -------------------------------------------------------------------------------- /test/unit/was_not_called.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.was-not-called 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f])) 6 | 7 | (u/deftest+ 8 | "passes if function was never called" 9 | (f/with-fakes 10 | (let [foo (f/recorded-fake)] 11 | (is (f/was-not-called foo))))) 12 | 13 | (u/deftest+ 14 | "throws if function was called once" 15 | (f/with-fakes 16 | (let [foo (f/recorded-fake [f/any nil])] 17 | (foo 5) 18 | (u/is-error-thrown 19 | #"^Function is expected to be never called\. Actual calls:\n\[\{:args \(5\), :return-value nil\}\]\." 20 | (f/was-not-called foo))))) 21 | 22 | (u/deftest+ 23 | "throws if function was called more than once" 24 | (f/with-fakes 25 | (let [foo (f/recorded-fake [f/any nil])] 26 | (foo) 27 | (foo 2) 28 | (u/is-error-thrown 29 | #"^Function is expected to be never called\. Actual calls:\n\[\{:args nil, :return-value nil\} \{:args \(2\), :return-value nil\}\]\." 30 | (f/was-not-called foo))))) -------------------------------------------------------------------------------- /test/unit/reporter.clj: -------------------------------------------------------------------------------- 1 | ; Prettify Clojure test reports 2 | (ns unit.reporter 3 | (:require 4 | [clojure.test :refer [report with-test-out]])) 5 | 6 | ; Change the report multimethod to ignore namespaces that don't contain any tests. 7 | ; taken from: http://blog.jayfields.com/2010/08/clojuretest-introduction.html 8 | (defmethod report :begin-test-ns [m] 9 | (with-test-out 10 | (when (some #(:test (meta %)) (vals (ns-interns (:ns m)))) 11 | (println "\n-------====== Testing" (ns-name (:ns m)) "======-------")))) 12 | 13 | (def ansi-reset "\u001B[0m") 14 | (def ansi-bold "\u001B[1m") 15 | (def ansi-red "\u001B[31m") 16 | 17 | ; Summary reporting with color 18 | (defmethod report :summary [m] 19 | (try 20 | (print ansi-bold) 21 | (when (not (every? zero? [(:fail m) (:error m)])) 22 | (print ansi-red)) 23 | 24 | (with-test-out 25 | (println "\nRan" (:test m) "tests containing" 26 | (+ (:pass m) (:fail m) (:error m)) "assertions.") 27 | (println (:fail m) "failures," (:error m) "errors.")) 28 | 29 | (finally 30 | (print ansi-reset)))) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yuri Govorushchenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /test/unit/fixtures/interop/InterfaceFixture.java: -------------------------------------------------------------------------------- 1 | package interop; 2 | 3 | public interface InterfaceFixture { 4 | int overloadedMethodWithDifferentReturnTypes(char x); 5 | int overloadedMethodWithDifferentReturnTypes(int x); 6 | int overloadedMethodWithDifferentReturnTypes(double x); 7 | String overloadedMethodWithDifferentReturnTypes(String x); 8 | String overloadedMethodWithDifferentReturnTypes(); 9 | String overloadedMethodWithDifferentReturnTypes(boolean x); 10 | boolean overloadedMethodWithDifferentReturnTypes(int x, int y); 11 | 12 | // With array arg 13 | String overloadedMethodWithDifferentReturnTypes(int[] x); 14 | String overloadedMethodWithDifferentReturnTypes(Integer[] x); 15 | 16 | // With array return value 17 | int[] overloadedMethodWithDifferentReturnTypes(int x, int y, int z); 18 | Integer[] overloadedMethodWithDifferentReturnTypes(double x, int y); 19 | 20 | // With varargs and arrays 21 | String overloadedMethodWithDifferentReturnTypes(int x, Integer... args); 22 | String overloadedMethodWithDifferentReturnTypes(Double... args); 23 | 24 | // Varargs (without overloading) 25 | String methodWithVarargs(int x, String... args); 26 | } -------------------------------------------------------------------------------- /test/unit/fixtures/macros.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.fixtures.macros 2 | (:require 3 | [clj-fakes.core :as f] 4 | [clj-fakes.context :as fc]) 5 | 6 | ; declare macros for export 7 | #?(:cljs (:require-macros 8 | [unit.fixtures.macros :refer 9 | [my-fake 10 | ctx-my-fake 11 | my-recorded-fake 12 | ctx-my-recorded-fake 13 | my-reify-fake 14 | ctx-my-reify-fake]]))) 15 | 16 | #?(:clj 17 | (defmacro my-fake 18 | [config] 19 | `(f/fake* ~&form ~config))) 20 | 21 | #?(:clj 22 | (defmacro ctx-my-fake 23 | [ctx config] 24 | `(fc/fake* ~ctx ~&form ~config))) 25 | 26 | #?(:clj 27 | (defmacro my-recorded-fake 28 | ([] `(f/recorded-fake* ~&form)) 29 | ([config] `(f/recorded-fake* ~&form ~config)))) 30 | 31 | #?(:clj 32 | (defmacro ctx-my-recorded-fake 33 | ([ctx] `(fc/recorded-fake* ~ctx ~&form)) 34 | ([ctx config] `(fc/recorded-fake* ~ctx ~&form ~config)))) 35 | 36 | #?(:clj 37 | (defmacro my-reify-fake 38 | [& specs] 39 | `(f/reify-fake* ~&form ~&env ~@specs))) 40 | 41 | #?(:clj 42 | (defmacro ctx-my-reify-fake 43 | [ctx & specs] 44 | `(fc/reify-fake* ~ctx ~&form ~&env ~@specs))) -------------------------------------------------------------------------------- /test/unit/cyclically.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.cyclically 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [unit.fixtures.protocols :as p])) 7 | 8 | (u/deftest+ 9 | "returned function cycles through specified values infinitely and takes any args" 10 | (let [foo (f/cyclically [:first :second :third])] 11 | (dotimes [_ 5] 12 | (is (= :first (foo))) 13 | (is (= :second (foo :some-arg))) 14 | (is (= :third (foo :some-arg1 :some-arg2)))))) 15 | 16 | (u/deftest+ 17 | "can be used for iterator-style stubbing" 18 | (f/with-fakes 19 | (let [get-weekday (f/fake [["My event"] (f/cyclically [:monday :tuesday :wednesday])])] 20 | (dotimes [_ 2] 21 | (is (= :monday (get-weekday "My event"))) 22 | (is (= :tuesday (get-weekday "My event"))) 23 | (is (= :wednesday (get-weekday "My event"))))))) 24 | 25 | (u/deftest+ 26 | "(just in case) can be used for iterator-style stubbing of methods" 27 | (f/with-fakes 28 | (let [cow (f/reify-fake 29 | p/AnimalProtocol 30 | (speak :fake [[] (f/cyclically ["moo" "mmm" "moo-moo"])]))] 31 | (dotimes [_ 2] 32 | (is (= "moo" (p/speak cow))) 33 | (is (= "mmm" (p/speak cow))) 34 | (is (= "moo-moo" (p/speak cow))))))) -------------------------------------------------------------------------------- /test/unit/method_was_not_called.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.method-was-not-called 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [unit.fixtures.protocols :as p])) 7 | 8 | (u/deftest+ 9 | "passes if function was never called" 10 | (f/with-fakes 11 | (let [cow (f/reify-fake p/AnimalProtocol 12 | (speak :recorded-fake))] 13 | (is (f/method-was-not-called p/speak cow))))) 14 | 15 | (u/deftest+ 16 | "throws if function was called once" 17 | (f/with-fakes 18 | (let [cow (f/reify-fake p/AnimalProtocol 19 | (speak :recorded-fake))] 20 | (p/speak cow 5) 21 | (u/is-error-thrown 22 | ; message is too complicated to assert here fully 23 | #"^Function is expected to be never called. Actual calls:\n.*\." 24 | (f/method-was-not-called p/speak cow))))) 25 | 26 | (u/deftest+ 27 | "throws if function was called more than once" 28 | (f/with-fakes 29 | (let [cow (f/reify-fake p/AnimalProtocol 30 | (speak :recorded-fake))] 31 | (p/speak cow) 32 | (p/speak cow 2) 33 | (u/is-error-thrown 34 | #"^Function is expected to be never called\. Actual calls:\n.*\." 35 | (f/method-was-not-called p/speak cow))))) -------------------------------------------------------------------------------- /docs/dev-guide.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | Autorun Clojure tests: `lein test-refresh` 4 | 5 | Run ClojureScript tests: `lein do clean, doo phantom test once` 6 | (clean is needed because there's an [issue](https://github.com/bensu/doo/issues/51): 7 | plugin does not seem to recompile macros) 8 | 9 | Use `min-deps` profile to test with minimal supported dependencies 10 | (instead of default and usually latest versions): 11 | 12 | lein with-profiles +min-deps test-refresh 13 | 14 | This profile cannot be applied to the ClojureScript version of the library 15 | because usage of the latest ClojureScript is always assumed. 16 | 17 | # Documentation 18 | 19 | Project uses [MkDocs](http://www.mkdocs.org/) 20 | with [Cinder](https://github.com/chrissimpkins/cinder) theme to generate documentation static site 21 | and [Codox](https://github.com/weavejester/codox) for API reference. 22 | 23 | Tasks are scripted using [Lumo](https://github.com/anmonteiro/lumo). 24 | Run `yarn` in order to install NodeJS dependencies for tasks. 25 | 26 | Build only site pages: `./tasks.cljs mkdocs` 27 | 28 | Build API reference into site folder: `./tasks.cljs api` 29 | 30 | Build the whole site: `./tasks.cljs site` 31 | 32 | Serve site pages locally with automatic build (but it won't work for index page): `mkdocs serve` 33 | 34 | # Deploying 35 | 36 | Deploy to Clojars: `lein deploy clojars` 37 | 38 | Deploy site to gh-pages branch: `ghp-import -p site` -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | 1) I assume your Clojure/ClojureScript project is automated using [Leiningen](http://leiningen.org/) and 4 | already has unit tests implemented with some unit testing framework. 5 | 6 | I will use a built-in 7 | [clojure.test/cljs.test](http://clojure.github.io/clojure/clojure.test-api.html) 8 | framework in this documentation. To learn how to use it: 9 | 10 | * in Clojure: see [Leiningen tutorial](https://github.com/technomancy/leiningen/blob/master/doc/TUTORIAL.md#tests) and 11 | [lein-test-refresh](https://github.com/jakemcc/lein-test-refresh) plugin; 12 | * in ClojureScript: see [wiki](https://github.com/clojure/clojurescript/wiki/Testing) and 13 | [doo](https://github.com/bensu/doo) plugin for running unit tests. 14 | 15 | 2) Add framework dependency into `project.clj` (the framework is hosted on [Clojars](https://clojars.org/clj-fakes)): 16 | 17 | ```clj 18 | :dependencies [... 19 | [clj-fakes "0.12.0"]] 20 | ``` 21 | 22 | 3) Require framework namespace in your unit test source file: 23 | 24 | ```clj 25 | (ns unit.example 26 | (:require 27 | [clojure.test :refer [is deftest]] 28 | [clj-fakes.core :as f])) 29 | ``` 30 | 31 | 4) Now you can write a simple unit test which creates and calls a fake function: 32 | 33 | ```clj 34 | (deftest fakes-work 35 | (f/with-fakes 36 | (let [hello (f/fake [[] "hello, world"])] 37 | (is (= "hello, world" (hello)))))) 38 | ``` -------------------------------------------------------------------------------- /tasks.cljs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lumo 2 | (ns core.tasks 3 | (:require 4 | [tasks.core :include-macros true :as t])) 5 | 6 | (def fs (js/require "fs-extra")) 7 | (def path (js/require "path")) 8 | 9 | (defn with-temp-copy 10 | "Copies file from source to dest, then calls the specified function. 11 | Finally, dest file is returned to its initial state." 12 | [source-path dest-path f] 13 | (let [dest-els (.parse path dest-path) 14 | backup-path (.join path 15 | (.-dir dest-els) 16 | (str (.-name dest-els) 17 | (.-ext dest-els) 18 | "_backup"))] 19 | (try 20 | (println "copy" dest-path "to" backup-path) 21 | (.copySync fs dest-path backup-path) 22 | (println "copy" source-path "to" dest-path) 23 | (.copySync fs source-path dest-path) 24 | (f) 25 | 26 | (finally 27 | (println "recover" dest-path "from" backup-path) 28 | (.copySync fs backup-path dest-path) 29 | 30 | (println "remove temp file" backup-path) 31 | (.removeSync fs backup-path))))) 32 | 33 | (defn ^:task api 34 | "Compile API reference into site folder." 35 | [] 36 | (t/run "lein" "codox")) 37 | 38 | (defn ^:task mkdocs 39 | "Only build site pages using MkDocs." 40 | [] 41 | ; use project's readme file for rendering the index page 42 | (with-temp-copy "README.md" (.join path "docs" "index.md") 43 | #(t/run "mkdocs" "build" "--clean"))) 44 | 45 | (defn ^:task site 46 | "Build project site (including API docs)." 47 | [] 48 | (mkdocs) 49 | (api)) 50 | 51 | (t/cli) -------------------------------------------------------------------------------- /test/unit/original_val.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.original-val 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [clj-fakes.context :as fc] 7 | [unit.fixtures.functions :as funcs])) 8 | 9 | (def my-var 111) 10 | (def my-nil-var nil) 11 | 12 | (u/deftest+ 13 | "original var val can be found" 14 | (f/with-fakes 15 | (f/patch! #'my-var 1000) 16 | (is (= 111 (f/original-val #'my-var))))) 17 | 18 | (u/deftest+ 19 | "original function val can be found" 20 | (f/with-fakes 21 | (is (not= 123 (funcs/sum 2 3))) 22 | (f/patch! #'funcs/sum (constantly 123)) 23 | 24 | (is (= 123 (funcs/sum 2 3))) 25 | (is (= 10 ((f/original-val #'funcs/sum) 7 3))))) 26 | 27 | (u/deftest+ 28 | "original function can be called by patched var" 29 | (f/with-fakes 30 | (f/patch! #'funcs/sum 31 | #((f/original-val #'funcs/sum) (* %1 %2) %2)) 32 | (is (= 16 (funcs/sum 3 4))))) 33 | 34 | (u/deftest+ 35 | "raises if key is not found" 36 | (f/with-fakes 37 | (u/is-assertion-error-thrown 38 | #"^Assert failed: Specified var is not patched\n" 39 | (f/original-val #'funcs/sum)))) 40 | 41 | (u/deftest+ 42 | "original value can be nil" 43 | (is (nil? my-nil-var) "self-test") 44 | (f/with-fakes 45 | (f/patch! #'my-nil-var 123) 46 | (is (nil? (f/original-val #'my-nil-var))))) 47 | 48 | (testing "works in explicit context" 49 | (let [ctx (fc/context) 50 | original-val my-var] 51 | (fc/patch! ctx #'my-var 200) 52 | (is (not= original-val my-var) "self test") 53 | 54 | (is (= original-val (fc/original-val ctx #'my-var))) 55 | (fc/unpatch-all! ctx))) -------------------------------------------------------------------------------- /test/unit/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns unit.runner 2 | (:require [cljs.test] 3 | [doo.runner :refer-macros [doo-tests]] 4 | [unit.context] 5 | [unit.args-matcher] 6 | [unit.optional-fake] 7 | [unit.fake] 8 | [unit.recorded-fake] 9 | [unit.unused-fakes-self-test] 10 | [unit.unchecked-fakes-self-test] 11 | [unit.was-called] 12 | [unit.was-called-once] 13 | [unit.was-matched-once] 14 | [unit.was-not-called] 15 | [unit.were-called-in-order] 16 | [unit.reify-fake] 17 | [unit.reify-nice-fake] 18 | [unit.positions] 19 | [unit.method-was-called] 20 | [unit.method-was-called-once] 21 | [unit.method-was-matched-once] 22 | [unit.method-was-not-called] 23 | [unit.methods-were-called-in-order] 24 | [unit.patch] 25 | [unit.original-val] 26 | [unit.unpatch] 27 | [unit.spy] 28 | [unit.cyclically] 29 | )) 30 | 31 | (doo-tests 32 | 'unit.context 33 | 'unit.args-matcher 34 | 'unit.optional-fake 35 | 'unit.fake 36 | 'unit.recorded-fake 37 | 'unit.unused-fakes-self-test 38 | 'unit.unchecked-fakes-self-test 39 | 'unit.was-called 40 | 'unit.was-called-once 41 | 'unit.was-matched-once 42 | 'unit.was-not-called 43 | 'unit.were-called-in-order 44 | 'unit.reify-fake 45 | 'unit.reify-nice-fake 46 | 'unit.positions 47 | 'unit.method-was-called 48 | 'unit.method-was-called-once 49 | 'unit.method-was-matched-once 50 | 'unit.method-was-not-called 51 | 'unit.methods-were-called-in-order 52 | 'unit.patch 53 | 'unit.original-val 54 | 'unit.unpatch 55 | 'unit.spy 56 | 'unit.cyclically 57 | ) 58 | -------------------------------------------------------------------------------- /test/unit/args_matcher.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.args-matcher 2 | (:require 3 | [clojure.test :refer [is are testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [clj-fakes.context :as fc])) 7 | 8 | (defn args-match? 9 | [matcher args] 10 | (is (satisfies? fc/ArgsMatcher matcher) "self test") 11 | (fc/args-match? matcher args)) 12 | 13 | (u/deftest+ 14 | "vector is args matcher" 15 | (are [v args] (args-match? v args) 16 | [11 22] [11 22] 17 | [] nil 18 | [1 nil] [1 nil] 19 | [nil nil] [nil nil]) 20 | 21 | (are [v args] (not (args-match? v args)) 22 | [2 3] [2 3 4] 23 | [2 3 4] [2 3] 24 | [1 2] nil) 25 | 26 | (testing "empty vector can be matched" 27 | (are [v args] (args-match? v args) 28 | [] [] 29 | [[]] [[]] 30 | [(f/arg integer?) []] [123 []]))) 31 | 32 | (u/deftest+ 33 | "function is an arg matcher" 34 | (are [v args] (args-match? v args) 35 | [100 (f/arg even?)] [100 2] 36 | [100 (f/arg even?)] [100 10]) 37 | 38 | (are [v args] (not (args-match? v args)) 39 | [100 (f/arg odd?)] [100 2] 40 | [100 (f/arg odd?)] [100 10])) 41 | 42 | (u/deftest+ 43 | "regex is an arg matcher" 44 | (are [args] (args-match? [(f/arg #"abc.*")] args) 45 | ["abc"] 46 | ["1abcd"] 47 | [" abc "]) 48 | 49 | (are [args] (not (args-match? [(f/arg #"abc.*")] args)) 50 | ["ab"] 51 | ["123ab4"] 52 | [" "])) 53 | 54 | (u/deftest+ 55 | "f/any args matcher matches everything" 56 | (are [args] (args-match? f/any args) 57 | nil 58 | [4] 59 | [3 2 4])) 60 | 61 | (u/deftest+ 62 | "f/any can be an arg matcher" 63 | (are [args] (args-match? [f/any 111] args) 64 | [nil 111] 65 | [[] 111] 66 | [2 111])) -------------------------------------------------------------------------------- /tasks/core.cljc: -------------------------------------------------------------------------------- 1 | (ns tasks.core 2 | (:require [clojure.string :as string] 3 | [cljs.analyzer.api])) 4 | 5 | (defn run 6 | "Runs the specified process." 7 | [command & args] 8 | (println "ᐅ" (str command " " (string/join " " args))) 9 | (let [result (.spawnSync (js/require "child_process") command (clj->js args) (clj->js {:stdio "inherit" :shell true}))] 10 | (when (not (= (.-status result) 0)) 11 | (throw (ex-info "Command exited with error code" {:status (.-status result)}))) 12 | 13 | (when (.-error result) 14 | (throw (ex-info "Error executing a command" {:error (.-error result)}))))) 15 | 16 | (defn cli* 17 | "tasks example: [{:name 'site' :fn site-fn :doc 'Lorem ipsum.'} ...]" 18 | [tasks] 19 | ; .-argv example: [/usr/local/Cellar/lumo/1.0.0/bin/lumo nexe.js ./tasks.cljc site] 20 | (let [task-name (get (.-argv js/process) 3) 21 | task-fn (->> tasks 22 | (filter #(= (:name %) task-name)) 23 | first 24 | :fn)] 25 | (when (nil? task-fn) 26 | (if (nil? task-name) 27 | (println "Error: no task specified") 28 | (println "Error: unknown task" (pr-str task-name))) 29 | 30 | (println) 31 | (println "Usage: task") 32 | (println "Available tasks:") 33 | (println) 34 | (println (->> tasks 35 | (map #(str "\t" (:name %) "\t" (:doc %))) 36 | (string/join "\n"))) 37 | (println) 38 | 39 | (.exit js/process 1)) 40 | 41 | (task-fn))) 42 | 43 | (defmacro cli 44 | "Executes the task passed into argv." 45 | [] 46 | (let [publics (cljs.analyzer.api/ns-publics (.-name *ns*)) 47 | task-publics (->> publics 48 | (filter #(-> % second :task))) 49 | tasks (mapv #(-> {:name (-> % second :name name) 50 | :fn (-> % second :name) 51 | :doc (-> % second :doc)}) 52 | task-publics)] 53 | `(cli* ~tasks))) -------------------------------------------------------------------------------- /test/unit/utils.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.utils 2 | (:require [clojure.string :as string] 3 | #?(:clj 4 | [clj-fakes.macro :as m])) 5 | 6 | ; declare macros for export 7 | #?(:cljs (:require-macros 8 | [unit.utils :refer 9 | [deftest+ 10 | is-exception-thrown 11 | is-error-thrown 12 | is-assertion-error-thrown]]))) 13 | 14 | #?(:clj 15 | (defmacro deftest+ 16 | "The same as deftest but name is defined using a string. 17 | Inspired by: https://gist.github.com/mybuddymichael/4425558" 18 | [name-string & body] 19 | (let [deftest (if (m/-cljs-env? &env) 'cljs.test/deftest 20 | 'clojure.test/deftest) 21 | name-symbol (-> name-string 22 | string/lower-case 23 | (string/replace #"\W" "-") 24 | (string/replace #"-+" "-") 25 | (string/replace #"-$" "") 26 | symbol)] 27 | `(~deftest ~name-symbol ~@body)))) 28 | 29 | #?(:clj 30 | (defmacro is-exception-thrown 31 | "(is (thrown-with-msg? ...)) for specified exceptions in Clojure/ClojureScript." 32 | [clj-exc-class cljs-exc-class re expr] 33 | (let [is (if (m/-cljs-env? &env) 'cljs.test/is 34 | 'clojure.test/is) 35 | exc-class (if (m/-cljs-env? &env) cljs-exc-class 36 | clj-exc-class)] 37 | `(~is (~'thrown-with-msg? ~exc-class ~re ~expr))))) 38 | 39 | #?(:clj 40 | (defmacro is-error-thrown 41 | "(is (thrown-with-msg? ...)) for general exceptions in Clojure/ClojureScript." 42 | [re expr] 43 | `(is-exception-thrown Exception js/Error ~re ~expr))) 44 | 45 | #?(:clj 46 | (defmacro is-assertion-error-thrown 47 | "(is (thrown-with-msg? ...)) for assert exceptions in Clojure/ClojureScript." 48 | [re expr] 49 | `(is-exception-thrown AssertionError js/Error ~re ~expr))) -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clj-fakes "0.12.0" 2 | :description "An isolation framework for Clojure/ClojureScript that makes creating test doubles (stubs, mocks, etc.) much easier." 3 | :url "https://github.com/metametadata/clj-fakes" 4 | :license {:name "MIT" :url "http://opensource.org/licenses/MIT"} 5 | 6 | :dependencies [[org.clojure/clojure "1.10.1" :scope "provided"] 7 | [org.clojure/clojurescript "1.10.238" :scope "provided"] 8 | 9 | ; For tests only 10 | [org.clojure/core.async "0.4.500" :scope "provided"]] 11 | 12 | :profiles {:min-deps {:dependencies [[org.clojure/clojure "1.7.0"] 13 | 14 | ; Fix conflicts 15 | [org.clojure/clojurescript "1.9.229" :exclusions [org.clojure/clojure]] 16 | [org.clojure/tools.reader "1.0.0-beta4"]]}} 17 | 18 | :plugins [[lein-cljsbuild "1.1.7"] 19 | [com.jakemccrary/lein-test-refresh "0.24.1"] 20 | [lein-doo "0.1.10" :exclusions [org.clojure/clojure]] 21 | [lein-codox "0.10.7" :exclusions [org.clojure/clojure]]] 22 | 23 | :pedantic? :abort 24 | 25 | :source-paths ["src" "test"] 26 | :java-source-paths ["test/unit/fixtures/interop"] 27 | 28 | :clean-targets ^{:protect false} [:target-path "resources/public/js/" "out"] 29 | 30 | :test-refresh {:notify-command ["terminal-notifier" "-title" "Tests" "-message"] 31 | :quiet true} 32 | 33 | :repositories {"clojars" {:sign-releases false}} 34 | 35 | :cljsbuild 36 | {:builds {:test {:source-paths ["src" "test" "test/unit" "test/unit/fixtures"] 37 | :compiler {:main unit.runner 38 | :output-to "resources/public/js/testable.js" 39 | :optimizations :none}}}} 40 | 41 | :codox {:source-uri "https://github.com/metametadata/clj-fakes/blob/master/{filepath}#L{line}" 42 | :source-paths ["src"] 43 | :namespaces [clj-fakes.core clj-fakes.context] 44 | :output-path "site/api" 45 | :metadata {:doc/format :markdown}}) 46 | -------------------------------------------------------------------------------- /test/unit/was_called_fn_contract.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.was-called-fn-contract 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [clj-fakes.context :as fc])) 7 | 8 | (defn test-was-called-fn-contract 9 | "Parametrized test which defines a contract for was-called-* funcs. 10 | Unfortunately it will short-circuit on first uncaught exception." 11 | [was-called-fn expected-exc-re-on-no-call] 12 | 13 | (testing "passes if function was called once" 14 | (f/with-fakes 15 | (let [foo (f/recorded-fake)] 16 | (foo) 17 | (is (was-called-fn foo []))))) 18 | 19 | (testing "throws if function was not called at all" 20 | (f/with-fakes 21 | (let [foo (f/recorded-fake)] 22 | (u/is-error-thrown 23 | expected-exc-re-on-no-call 24 | (was-called-fn foo []))))) 25 | 26 | (testing "args matcher can be specified" 27 | (f/with-fakes 28 | (let [foo (f/recorded-fake)] 29 | (foo 2 3) 30 | (is (was-called-fn foo (reify fc/ArgsMatcher 31 | (args-match? [_ args] 32 | (= [2 3] args)))))))) 33 | 34 | (testing "throws if function was never called with specified args" 35 | (f/with-fakes 36 | (let [foo (f/recorded-fake [f/any nil])] 37 | (foo 2 3) 38 | (u/is-error-thrown 39 | #"^Function was never called with the expected args\.\nArgs matcher: \[2 4\]\.\nActual calls:\n\[\{:args \(2 3\), :return-value nil\}\]\n" 40 | (was-called-fn foo [2 4]))))) 41 | 42 | (testing "(regression) check with matcher should not pass when function was called with no args" 43 | (f/with-fakes 44 | (let [foo (f/recorded-fake [f/any nil])] 45 | (foo) 46 | (u/is-error-thrown 47 | #"^Function was never called with the expected args\.\nArgs matcher: \[1 2\]\.\nActual calls:\n\[\{:args nil, :return-value nil\}\]\n" 48 | (was-called-fn foo [1 2]))))) 49 | 50 | (testing "on exception args matcher with any, function and regex arg matchers is printed in a readable form" 51 | (f/with-fakes 52 | (let [foo (f/recorded-fake [f/any nil])] 53 | (foo 2 3) 54 | (u/is-error-thrown 55 | #"^Function was never called with the expected args\.\nArgs matcher: \[2 4 \]\.\nActual calls:\n\[\{:args \(2 3\), :return-value nil\}\]\n" 56 | (was-called-fn foo [2 4 f/any (f/arg string?) (f/arg #"abc")]))))) 57 | ) -------------------------------------------------------------------------------- /test/unit/method_was_called_fn_contract.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.method-was-called-fn-contract 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [clj-fakes.context :as fc] 7 | [unit.fixtures.protocols :as p])) 8 | 9 | (defn test-method-was-called-fn-contract 10 | "Parametrized test which defines a contract for method-was-called-* funcs. 11 | Unfortunately it will short-circuit on first uncaught exception." 12 | [was-called-fn expected-exc-re-on-no-call] 13 | 14 | (testing "passes if method was called once" 15 | (f/with-fakes 16 | (let [cow (f/reify-fake p/AnimalProtocol 17 | (speak :recorded-fake))] 18 | (p/speak cow) 19 | (is (was-called-fn p/speak cow []))))) 20 | 21 | (testing "args matcher can be specified" 22 | (f/with-fakes 23 | (let [cow (f/reify-fake p/AnimalProtocol 24 | (speak :recorded-fake))] 25 | (p/speak cow 2 3) 26 | (is (was-called-fn p/speak cow (reify fc/ArgsMatcher 27 | (args-match? [_ args] 28 | (= [2 3] args)))))))) 29 | 30 | (testing "throws if function was not called at all" 31 | (f/with-fakes 32 | (let [cow (f/reify-fake p/AnimalProtocol 33 | (speak :recorded-fake))] 34 | (u/is-error-thrown 35 | expected-exc-re-on-no-call 36 | (was-called-fn p/speak cow []))))) 37 | 38 | (testing "throws if function was never called with specified args" 39 | (f/with-fakes 40 | (let [cow (f/reify-fake p/AnimalProtocol 41 | (speak :recorded-fake [f/any nil]))] 42 | (p/speak cow 2 3) 43 | (u/is-error-thrown 44 | ; message is too complicated to assert here fully 45 | #"^Function was never called with the expected args\.\nArgs matcher: \[2 4\]\.\nActual calls:\n.*\n" 46 | (was-called-fn p/speak cow [2 4]))))) 47 | 48 | (testing "on exception args matcher with any, function and regex arg matchers is printed in a readable form" 49 | (f/with-fakes 50 | (let [cow (f/reify-fake p/AnimalProtocol 51 | (speak :recorded-fake [f/any nil]))] 52 | (p/speak cow 2 3) 53 | (u/is-error-thrown 54 | ; message is too complicated to assert here fully 55 | #"^Function was never called with the expected args\.\nArgs matcher: \[2 4 \]\.\nActual calls:\n.*\n" 56 | (was-called-fn p/speak cow [2 4 f/any (f/arg string?) (f/arg #"abc")]))))) 57 | ) -------------------------------------------------------------------------------- /test/unit/methods_were_called_in_order.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.methods-were-called-in-order 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [unit.fixtures.protocols :as p])) 7 | 8 | (u/deftest+ 9 | "big integration test" 10 | (f/with-fakes 11 | (let [cow (f/reify-fake p/AnimalProtocol 12 | (eat :recorded-fake) 13 | (speak :recorded-fake) 14 | (sleep :recorded-fake)) 15 | dog (f/reify-fake p/AnimalProtocol 16 | (eat :recorded-fake))] 17 | (p/speak cow) 18 | (p/sleep cow) 19 | (p/eat dog "dog food" "water") 20 | (p/speak cow "Bob") 21 | (p/eat cow "grass" "cola") 22 | (p/speak cow "John" "Alice") 23 | 24 | (is (f/methods-were-called-in-order 25 | p/speak cow [])) 26 | 27 | (is (f/methods-were-called-in-order 28 | p/speak cow [] 29 | p/sleep cow [])) 30 | 31 | (is (f/methods-were-called-in-order 32 | p/speak cow [] 33 | p/speak cow ["John" "Alice"])) 34 | 35 | (is (f/methods-were-called-in-order 36 | p/eat cow ["grass" "cola"] 37 | p/speak cow ["John" "Alice"])) 38 | 39 | (is (f/methods-were-called-in-order 40 | p/speak cow [(f/arg string?)] 41 | p/eat cow [(f/arg string?) (f/arg string?)] 42 | p/speak cow [(f/arg string?) (f/arg string?)])) 43 | 44 | (is (f/methods-were-called-in-order 45 | p/speak cow [] 46 | p/sleep cow [] 47 | p/eat dog ["dog food" "water"] 48 | p/speak cow ["Bob"] 49 | p/eat cow ["grass" "cola"] 50 | p/speak cow ["John" "Alice"]))))) 51 | 52 | (u/deftest+ 53 | "throws if different functions were not called in expected order" 54 | (f/with-fakes 55 | (let [cow (f/reify-fake p/AnimalProtocol 56 | (speak :recorded-fake)) 57 | dog (f/reify-fake p/AnimalProtocol 58 | (eat :recorded-fake))] 59 | (p/speak cow "Bob") 60 | (p/eat dog "dog food" "water") 61 | 62 | (u/is-error-thrown 63 | (re-pattern (str "^Could not find a call satisfying step #2:\n" 64 | "recorded fake from .*unit/methods_were_called_in_order\\.cljc" 65 | ", 55:15 \\(p/AnimalProtocol, speak\\)\n" 66 | "args matcher: \\[Bob\\]$")) 67 | (f/methods-were-called-in-order 68 | p/eat dog ["dog food" "water"] 69 | p/speak cow ["Bob"]))))) -------------------------------------------------------------------------------- /src/clj_fakes/reflection.clj: -------------------------------------------------------------------------------- 1 | (ns clj-fakes.reflection 2 | (:require [clj-fakes.macro :as m]) 3 | (:import (java.lang.reflect Method))) 4 | 5 | (defn -cljs-resolve 6 | "Alternative way to call cljs.analyzer/resolve-var. 7 | It's needed to be able to compile the library in the Clojure-only project." 8 | [env sym] 9 | ((ns-resolve 'cljs.analyzer 'resolve-var) env sym)) 10 | 11 | (defn -resolves? 12 | [env sym] 13 | (if (m/-cljs-env? env) 14 | ; ClojureScript 15 | (not (nil? (:meta (-cljs-resolve env sym)))) 16 | 17 | ; Clojure 18 | (not (nil? (resolve sym))))) 19 | 20 | (defn -resolve-protocol-with-specs 21 | "Returns a resolved protocol or nil if resolved object has no protocol specs." 22 | [env protocol-sym] 23 | (if (m/-cljs-env? env) 24 | ; ClojureScript 25 | (let [protocol (-cljs-resolve env protocol-sym)] 26 | (when-not (nil? (-> protocol :meta :protocol-info)) 27 | protocol)) 28 | 29 | ; Clojure 30 | (let [protocol (resolve protocol-sym)] 31 | (when (instance? clojure.lang.Var protocol) 32 | protocol)))) 33 | 34 | (defn -protocol-methods 35 | "Portable reflection helper. Returns different structures for different hosts. 36 | Protocol must be already resolved." 37 | [env protocol] 38 | (if (m/-cljs-env? env) 39 | ; ClojureScript 40 | (-> protocol :meta :protocol-info :methods) 41 | 42 | ; Clojure 43 | (-> protocol deref :sigs vals))) 44 | 45 | (defn -protocol-method-name 46 | "Portable reflection helper." 47 | [env protocol-method] 48 | (if (m/-cljs-env? env) 49 | ; ClojureScript 50 | (first protocol-method) 51 | 52 | ; Clojure 53 | (:name protocol-method))) 54 | 55 | (defn -protocol-method-arglist 56 | "Portable reflection helper." 57 | [env protocol-method] 58 | (if (m/-cljs-env? env) 59 | ; ClojureScript 60 | (second protocol-method) 61 | 62 | ; Clojure 63 | (:arglists protocol-method))) 64 | 65 | (defn -interface-or-object-methods 66 | "Raises an exception if cannot reflect on specified symbol." 67 | [env interface-sym] 68 | (assert (not (m/-cljs-env? env)) "Works only in Clojure for reflection on Java interfaces and Object class.") 69 | ; Not using clojure.reflect because it returns types in a form cumbersome to transform into type hints. 70 | ; For instance, it returns "java.lang.Integer<>" but the hint should be "[Ljava.lang.Integer;". 71 | (let [class (try 72 | (if (= interface-sym 'Object) 73 | Object 74 | (Class/forName (str interface-sym))) 75 | 76 | (catch Exception e 77 | (assert nil (str "Unknown protocol or interface: " interface-sym 78 | ". Underlying exception: " (pr-str e)))))] 79 | (for [^Method method (.getMethods class)] 80 | {:name (.getName method) 81 | :return-type (.getReturnType method) 82 | :parameter-types (.getParameterTypes method)}))) -------------------------------------------------------------------------------- /test/unit/unpatch.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.unpatch 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [clj-fakes.context :as fc])) 7 | 8 | (def my-var1 111) 9 | (def my-var2 222) 10 | 11 | (u/deftest+ 12 | "var can be unpatched explicitly" 13 | (let [original-val my-var1] 14 | (f/with-fakes 15 | (f/patch! #'my-var1 200) 16 | (is (not= original-val my-var1) "self test") 17 | 18 | (f/unpatch! #'my-var1) 19 | (is (= original-val my-var1))) 20 | 21 | (is (= original-val my-var1) "just in case"))) 22 | 23 | (u/deftest+ 24 | "unpatch works in explicit context" 25 | (let [ctx (fc/context) 26 | original-val my-var1] 27 | (fc/patch! ctx #'my-var1 200) 28 | (is (not= original-val my-var1) "self test") 29 | 30 | (fc/unpatch! ctx #'my-var1) 31 | (is (= original-val my-var1)))) 32 | 33 | (u/deftest+ 34 | "var can be unpatched twice in a row" 35 | (let [original-val my-var1] 36 | (f/with-fakes 37 | (f/patch! #'my-var1 200) 38 | (is (not= original-val my-var1) "self test") 39 | 40 | (f/unpatch! #'my-var1) 41 | (f/unpatch! #'my-var1) 42 | (is (= original-val my-var1))) 43 | 44 | (is (= original-val my-var1) "just in case"))) 45 | 46 | (u/deftest+ 47 | "var can be patched/unpatched several times" 48 | (let [original-val my-var1] 49 | (f/with-fakes 50 | (f/patch! #'my-var1 200) 51 | (is (not= original-val my-var1) "self test") 52 | (f/unpatch! #'my-var1) 53 | (is (= original-val my-var1)) 54 | 55 | (f/patch! #'my-var1 300) 56 | (is (not= original-val my-var1) "self test") 57 | (f/unpatch! #'my-var1) 58 | (is (= original-val my-var1)) 59 | 60 | (f/patch! #'my-var1 400) 61 | (is (not= original-val my-var1) "self test") 62 | (f/unpatch! #'my-var1) 63 | (is (= original-val my-var1))) 64 | 65 | (is (= original-val my-var1) "just in case"))) 66 | 67 | (u/deftest+ 68 | "raises if key is not found" 69 | (f/with-fakes 70 | (u/is-assertion-error-thrown 71 | #"^Assert failed: Specified var is not patched\n" 72 | (f/unpatch! #'my-var1)))) 73 | 74 | (u/deftest+ 75 | "all vars can be unpatched at once" 76 | (let [original-val1 my-var1 77 | original-val2 my-var2] 78 | (f/with-fakes 79 | (f/patch! #'my-var1 200) 80 | (f/patch! #'my-var2 300) 81 | (is (not= original-val1 my-var1) "self test") 82 | (is (not= original-val2 my-var2) "self test") 83 | 84 | (f/unpatch-all!) 85 | (is (= original-val1 my-var1)) 86 | (is (= original-val2 my-var2))) 87 | 88 | (is (= original-val1 my-var1) "just in case") 89 | (is (= original-val2 my-var2) "just in case"))) 90 | 91 | (u/deftest+ 92 | "unpatch-all works in explicit context" 93 | (let [ctx (fc/context) 94 | original-val my-var1] 95 | (fc/patch! ctx #'my-var1 200) 96 | (is (not= original-val my-var1) "self test") 97 | 98 | (fc/unpatch-all! ctx) 99 | (is (= original-val my-var1)))) -------------------------------------------------------------------------------- /test/unit/context.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.context 2 | (:require 3 | [clojure.test :refer [is testing #?(:cljs async)]] 4 | [unit.utils :as u] 5 | [clojure.core.async :as a] 6 | [clj-fakes.core :as f])) 7 | 8 | (def my-var 111) 9 | 10 | (u/deftest+ 11 | "context executes its body and returns last expression" 12 | (let [x (atom 100) 13 | return-val (f/with-fakes 14 | (reset! x 200) 15 | (inc @x))] 16 | (is (= return-val 201)))) 17 | 18 | (u/deftest+ 19 | "contexts can nest" 20 | (f/with-fakes 21 | (is (= 111 my-var)) 22 | (f/patch! #'my-var "parent context") 23 | (is (= "parent context" my-var)) 24 | 25 | (f/with-fakes 26 | (is (= "parent context" my-var)) 27 | (f/patch! #'my-var "child context") 28 | (is (= "child context" my-var))) 29 | 30 | (is (= "parent context" my-var)))) 31 | 32 | (u/deftest+ 33 | "function can be used instead of a macro" 34 | (f/with-fakes* 35 | (fn [] (is (= 111 my-var)) 36 | (f/patch! #'my-var "parent context") 37 | (is (= "parent context" my-var)) 38 | 39 | (f/with-fakes* 40 | (fn [new-name] 41 | (is (= "parent context" my-var)) 42 | (f/patch! #'my-var new-name) 43 | (is (= "child context" my-var))) 44 | "child context") 45 | 46 | (is (= "parent context" my-var))))) 47 | 48 | (u/deftest+ 49 | "with-fakes macro resets the binding on exiting the block" 50 | (is (nil? f/*context*) "self-test: context is nil by default") 51 | (f/with-fakes 52 | (is (some? f/*context*) "self-test: context is bound inside the block")) 53 | 54 | (is (nil? f/*context*) "context is unbound on exiting the block")) 55 | 56 | (u/deftest+ 57 | "with-fakes* function resets the binding on exiting the block" 58 | (is (nil? f/*context*) "self-test: context is nil by default") 59 | (f/with-fakes* 60 | #(is (some? f/*context*) "self-test: context is bound inside the block")) 61 | 62 | (is (nil? f/*context*) "context is unbound on exiting the block")) 63 | 64 | #?(:cljs 65 | (u/deftest+ 66 | "with-fakes cannot be used inside a go-block in ClojureScript" 67 | (async done 68 | (a/go 69 | (try 70 | (u/is-assertion-error-thrown 71 | #"with-fakes cannot be used here" 72 | (f/with-fakes)) 73 | 74 | (finally 75 | (done))))))) 76 | 77 | #?(:clj 78 | (u/deftest+ 79 | "with-fakes can be used inside a go-block in Clojure" 80 | (a/, rest: [1 2 3]. 61 | Actual calls: ... 62 | ``` 63 | 64 | ``` 65 | expected: (f/method-was-called p/speak cow [1 2 3]) 66 | actual: clojure.lang.ExceptionInfo: Function was never called with the expected args. 67 | Args matcher: [1 2 3]. 68 | Actual calls: ... 69 | ``` 70 | 71 | - New function `clj-fakes.context/self-test` will run all available self-tests. 72 | - Better arg naming in API. 73 | 74 | ## 0.4.0 75 | 76 | Fixed: 77 | - ClojureScript is a hard dependency (#1). 78 | 79 | ## 0.3.0 80 | 81 | - Public release. -------------------------------------------------------------------------------- /test/unit/unused_fakes_self_test.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.unused-fakes-self-test 2 | (:require 3 | [clojure.test :refer [is testing #?(:cljs async)]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [clj-fakes.context :as fc] 7 | [unit.fixtures.protocols :as p])) 8 | 9 | (u/deftest+ 10 | "user is warned if fake was never called" 11 | (u/is-error-thrown 12 | #"^Self-test: no call detected for:\nnon-optional fake from .*unit/unused_fakes_self_test\.cljc, 14:7" 13 | (f/with-fakes 14 | (f/fake [f/any nil])))) 15 | 16 | (u/deftest+ 17 | "user is not warned if fake was never called in case of exception inside the context" 18 | (u/is-error-thrown 19 | #"expected" 20 | (f/with-fakes 21 | (f/fake [f/any nil]) 22 | (throw (ex-info "expected" {}))))) 23 | 24 | (u/deftest+ 25 | "user is not warned if fake was never called in case of assertion error inside the context" 26 | (u/is-assertion-error-thrown 27 | #"expected" 28 | (f/with-fakes 29 | (f/fake [f/any nil]) 30 | (assert nil "expected")))) 31 | 32 | #?(:cljs 33 | (u/deftest+ 34 | "user is not warned if fake was never called in case of non-object exception inside the context" 35 | (let [caught-exception (atom nil)] 36 | (try 37 | (f/with-fakes 38 | (f/fake [f/any nil]) 39 | (throw "expected")) 40 | (catch :default e 41 | (reset! caught-exception e)) 42 | (finally 43 | (is (= "expected" @caught-exception))))))) 44 | 45 | (u/deftest+ 46 | "works with explicit context" 47 | (u/is-error-thrown 48 | #"^Self-test: no call detected for:\nnon-optional fake from .*unit/unused_fakes_self_test\.cljc, 50:7" 49 | (let [ctx (fc/context)] 50 | (fc/fake ctx [f/any nil]) 51 | (fc/self-test-unused-fakes ctx)))) 52 | 53 | (u/deftest+ 54 | "user is not warned if optional fake was never called" 55 | (f/with-fakes 56 | (f/optional-fake [f/any nil]))) 57 | 58 | (u/deftest+ 59 | "user is warned if several fakes were never called" 60 | (u/is-error-thrown 61 | #"^Self-test: no call detected for: 62 | non-optional fake from .*unit/unused_fakes_self_test\.cljc, 66:7 63 | non-optional fake from .*unit/unused_fakes_self_test\.cljc, 67:7 64 | non-optional fake from .*unit/unused_fakes_self_test\.cljc, 68:7" 65 | (f/with-fakes 66 | (f/fake [f/any nil]) 67 | (f/fake [f/any nil]) 68 | (f/fake [f/any nil])))) 69 | 70 | ;;;;;;;;;;;;;;;;;;;;;;;;;; reify-fake 71 | (u/deftest+ 72 | "user is not warned if reified protocol fake was never called" 73 | (u/is-error-thrown 74 | #"^Self-test: no call detected for:\nnon-optional fake from .*unit/unused_fakes_self_test\.cljc, 76:7 \(p/AnimalProtocol, speak\)" 75 | (f/with-fakes 76 | (f/reify-fake p/AnimalProtocol 77 | (speak :fake [f/any nil]))))) 78 | 79 | (u/deftest+ 80 | "user is not warned if reified protocol fake was never called (explicit context)" 81 | (u/is-error-thrown 82 | #"^Self-test: no call detected for:\nnon-optional fake from .*unit/unused_fakes_self_test\.cljc, 84:7 \(p/AnimalProtocol, speak\)" 83 | (let [ctx (fc/context)] 84 | (fc/reify-fake ctx p/AnimalProtocol 85 | (speak :fake [f/any nil])) 86 | (fc/self-test-unused-fakes ctx)))) -------------------------------------------------------------------------------- /test/unit/unchecked_fakes_self_test.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.unchecked-fakes-self-test 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [clj-fakes.context :as fc] 7 | [unit.fixtures.protocols :as p])) 8 | 9 | (u/deftest+ 10 | "user is warned if fake was never checked" 11 | (u/is-error-thrown 12 | #"^Self-test: no check performed on:\nrecorded fake from .*unit/unchecked_fakes_self_test\.cljc, 14:7" 13 | (f/with-fakes 14 | (f/recorded-fake [f/any nil])))) 15 | 16 | (u/deftest+ 17 | "user is not warned if fake was never called if it's explicitly marked as checked" 18 | (f/with-fakes 19 | (f/mark-checked (f/recorded-fake [f/any nil])))) 20 | 21 | (u/deftest+ 22 | "user is not warned if fake was never checked in case of exception inside the context" 23 | (u/is-error-thrown 24 | #"expected" 25 | (f/with-fakes 26 | (f/recorded-fake [f/any nil]) 27 | (throw (ex-info "expected" {}))))) 28 | 29 | (u/deftest+ 30 | "user is not warned if fake was never checked in case of assertion error inside the context" 31 | (u/is-assertion-error-thrown 32 | #"expected" 33 | (f/with-fakes 34 | (f/recorded-fake [f/any nil]) 35 | (assert nil "expected")))) 36 | 37 | #?(:cljs 38 | (u/deftest+ 39 | "user is not warned if fake was never checked in case of non-object exception inside the context" 40 | (let [caught-exception (atom nil)] 41 | (try 42 | (f/with-fakes 43 | (f/recorded-fake [f/any nil]) 44 | (throw "expected")) 45 | (catch :default e 46 | (reset! caught-exception e)) 47 | (finally 48 | (is (= "expected" @caught-exception))))))) 49 | 50 | (u/deftest+ 51 | "self-test works with explicit context" 52 | (u/is-error-thrown 53 | #"^Self-test: no check performed on:\nrecorded fake from .*unit/unchecked_fakes_self_test\.cljc, 55:7" 54 | (let [ctx (fc/context)] 55 | (fc/recorded-fake ctx [f/any nil]) 56 | (fc/self-test-unchecked-fakes ctx)))) 57 | 58 | (u/deftest+ 59 | "self-test about unchecked recorded fakes is more important than the one about unused fakes" 60 | (u/is-error-thrown 61 | #"^Self-test: no check performed on:\nrecorded fake from .*unit/unchecked_fakes_self_test\.cljc, 64:7" 62 | (f/with-fakes 63 | (f/fake [f/any nil]) 64 | (f/recorded-fake [f/any nil])))) 65 | 66 | (u/deftest+ 67 | "user is warned if several fakes were not checked" 68 | (u/is-error-thrown 69 | #"^Self-test: no check performed on:\nrecorded fake from .*unit/unchecked_fakes_self_test\.cljc, 71:7\nrecorded fake from .*unit/unchecked_fakes_self_test\.cljc, 72:7\nrecorded fake from .*unit/unchecked_fakes_self_test\.cljc, 73:7$" 70 | (f/with-fakes 71 | (f/recorded-fake [f/any nil]) 72 | (f/recorded-fake [f/any nil]) 73 | (f/recorded-fake [f/any nil])))) 74 | 75 | ;;;;;;;;;;;;;;;;;;;;;;;;;; reify-fake 76 | (u/deftest+ 77 | "user is warned if reified protocol fake was never checked" 78 | (u/is-error-thrown 79 | #"^Self-test: no check performed on:\nrecorded fake from .*unit/unchecked_fakes_self_test\.cljc, 81:7 \(p/AnimalProtocol, speak\)" 80 | (f/with-fakes 81 | (f/reify-fake p/AnimalProtocol 82 | (speak :recorded-fake [f/any nil]))))) 83 | 84 | (u/deftest+ 85 | "user is warned if reified protocol fake was never checked (explicit context)" 86 | (u/is-error-thrown 87 | #"^Self-test: no check performed on:\nrecorded fake from .*unit/unchecked_fakes_self_test\.cljc, 89:7 \(p/AnimalProtocol, speak\)" 88 | (let [ctx (fc/context)] 89 | (fc/reify-fake ctx p/AnimalProtocol 90 | (speak :recorded-fake [f/any nil])) 91 | (fc/self-test-unchecked-fakes ctx)))) -------------------------------------------------------------------------------- /test/unit/recorded_fake.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.recorded-fake 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [unit.fake-fn-contract :as c] 6 | [clj-fakes.context :as fc] 7 | [clj-fakes.core :as f])) 8 | 9 | (defn recorded-fake 10 | ([] 11 | (let [fake-fn (f/recorded-fake)] 12 | ; supress self-test warnings 13 | (f/mark-checked fake-fn) 14 | fake-fn)) 15 | 16 | ([config] 17 | (let [fake-fn (f/recorded-fake config)] 18 | ; supress self-test warnings 19 | (f/mark-checked fake-fn) 20 | fake-fn))) 21 | 22 | ; we can't pass a macro as an arg thus it's wrapped into a function 23 | (defn ctx-recorded-fake 24 | ([ctx] (fc/recorded-fake ctx)) 25 | ([ctx config] (fc/recorded-fake ctx config))) 26 | 27 | (u/deftest+ 28 | "fake contract" 29 | (c/test-fake-fn-contract 30 | recorded-fake 31 | ctx-recorded-fake 32 | false)) 33 | 34 | (u/deftest+ 35 | "there are no calls recorded if fake was not called" 36 | (f/with-fakes 37 | (let [foo (f/recorded-fake [[] 123])] 38 | (f/mark-checked foo) 39 | (is (= [] (f/calls foo))) 40 | (is (= [] (f/calls)))))) 41 | 42 | (u/deftest+ 43 | "call args and return values are recorded on single call without args" 44 | (f/with-fakes 45 | (let [foo (f/recorded-fake [[] 123])] 46 | (f/mark-checked foo) 47 | 48 | (foo) 49 | 50 | (is (= [{:args nil :return-value 123}] 51 | (f/calls foo))) 52 | (is (= [[foo {:args nil :return-value 123}]] 53 | (f/calls)))))) 54 | 55 | (u/deftest+ 56 | "call args and return values are recorded on single call with args" 57 | (f/with-fakes 58 | (let [foo (f/recorded-fake [f/any 123])] 59 | (f/mark-checked foo) 60 | 61 | (foo 100 200) 62 | 63 | (is (= [{:args '(100 200) :return-value 123}] 64 | (f/calls foo))) 65 | (is (= [[foo {:args '(100 200) :return-value 123}]] 66 | (f/calls)))))) 67 | 68 | (u/deftest+ 69 | "call args and return values are recorded on several calls" 70 | (f/with-fakes 71 | (let [foo (f/recorded-fake [f/any #(- %1 %2)])] 72 | (f/mark-checked foo) 73 | 74 | (foo 200 100) 75 | (foo 500 300) 76 | (foo 900 500) 77 | 78 | (is (= [{:args '(200 100) :return-value 100} 79 | {:args '(500 300) :return-value 200} 80 | {:args '(900 500) :return-value 400}] 81 | (f/calls foo))) 82 | (is (= [[foo {:args '(200 100) :return-value 100}] 83 | [foo {:args '(500 300) :return-value 200}] 84 | [foo {:args '(900 500) :return-value 400}]] 85 | (f/calls)))))) 86 | 87 | (u/deftest+ 88 | "call args and return values are recorded for several fakes" 89 | (f/with-fakes 90 | (let [foo (f/recorded-fake [[(f/arg integer?) (f/arg integer?)] #(+ %1 %2)]) 91 | bar (f/recorded-fake [[(f/arg integer?) (f/arg integer?)] #(* %1 %2)])] 92 | (f/mark-checked foo) 93 | (f/mark-checked bar) 94 | 95 | (foo 1 2) 96 | (foo 3 4) 97 | (bar 5 6) 98 | (foo 7 8) 99 | (bar 9 10) 100 | 101 | (is (= [{:args [1 2] :return-value 3} 102 | {:args [3 4] :return-value 7} 103 | {:args [7 8] :return-value 15}] 104 | (f/calls foo))) 105 | 106 | (is (= [{:args [5 6] :return-value 30} 107 | {:args [9 10] :return-value 90}] 108 | (f/calls bar))) 109 | (is (= [[foo {:args [1 2] :return-value 3}] 110 | [foo {:args [3 4] :return-value 7}] 111 | [bar {:args [5 6] :return-value 30}] 112 | [foo {:args [7 8] :return-value 15}] 113 | [bar {:args [9 10] :return-value 90}]] 114 | (f/calls)))))) 115 | 116 | (u/deftest+ 117 | "(just in case) calls are not recorded for other types of fakes" 118 | (f/with-fakes 119 | (let [foo (f/optional-fake [f/any nil]) 120 | bar (f/fake [f/any nil])] 121 | (foo) 122 | (bar) 123 | 124 | (is (= [] (f/calls)))))) 125 | 126 | (u/deftest+ 127 | "(just in case) calls are still recorded if config is not specified" 128 | (f/with-fakes 129 | (let [foo (f/recorded-fake)] 130 | (f/mark-checked foo) 131 | 132 | (let [result (foo 1 2 3)] 133 | (is (= [{:args [1 2 3] :return-value result}] (f/calls foo))))))) -------------------------------------------------------------------------------- /test/unit/fake_fn_contract.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.fake-fn-contract 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [clj-fakes.context :as fc])) 7 | 8 | (defn test-fake-fn-contract 9 | "Parametrized test which defines a contract for all function fakes. 10 | Unfortunately it will short-circuit on first uncaught exception." 11 | [fake-fn ctx-fake-fn config-required?] 12 | 13 | (testing "fake can call a function for the simplest matcher" 14 | (f/with-fakes 15 | (let [foo (fake-fn [(reify fc/ArgsMatcher 16 | (args-match? [_ _] true)) 17 | #(* %1 %2)])] 18 | (is (= 242 (foo 11 22)))))) 19 | 20 | (testing "matcher recieves fake's call args" 21 | (f/with-fakes 22 | (let [foo (fake-fn [(reify fc/ArgsMatcher 23 | (args-match? [_ args] (= [5 6] args)) 24 | (args-matcher->str [_] "")) 25 | #(* %1 %2)])] 26 | (is (= 30 (foo 5 6)))))) 27 | 28 | (testing "works in explicit context" 29 | (let [ctx (fc/context) 30 | foo (ctx-fake-fn ctx [(reify fc/ArgsMatcher 31 | (args-match? [_ args] (= [5 6] args)) 32 | (args-matcher->str [_] "")) 33 | #(* %1 %2)])] 34 | (is (= 30 (foo 5 6))))) 35 | 36 | (testing "matcher recieves nil on no call args" 37 | (f/with-fakes 38 | (let [foo (fake-fn [(reify fc/ArgsMatcher 39 | (args-match? [_ args] (nil? args)) 40 | (args-matcher->str [_] "")) 41 | 500])] 42 | (is (= 500 (foo)))))) 43 | 44 | (testing "calling fake on unsupported args raises an exception" 45 | (f/with-fakes 46 | (let [foo (fake-fn [(reify fc/ArgsMatcher 47 | (args-match? [_ _] false) 48 | (args-matcher->str [_] "")) 49 | (fn [_ _])])] 50 | (u/is-error-thrown 51 | #"^Unexpected args are passed into fake: \(2 3\).\nSupported args matchers:\n" 52 | (foo 2 3))))) 53 | 54 | (testing "fake can return a fixed value instead of calling a function" 55 | (f/with-fakes 56 | (let [foo (fake-fn [[11 22] 111])] 57 | (is (= 111 (foo 11 22)))))) 58 | 59 | (testing "fake can call different functions for different arg combinations" 60 | (f/with-fakes 61 | (let [foo (fake-fn [[11 22] (fn [x y] (* x y)) 62 | [8 4] (fn [x y] (- x y)) 63 | [100 5] (fn [x y] (/ x y))])] 64 | (is (= 242 (foo 11 22))) 65 | (is (= 4 (foo 8 4))) 66 | (is (= 20 (foo 100 5)))))) 67 | 68 | (testing "fake can return fixed values for different arg combinations" 69 | (f/with-fakes 70 | (let [foo (fake-fn [[11 22] "wow" 71 | [8 4] "hey" 72 | [100 5] 123])] 73 | (is (= "wow" (foo 11 22))) 74 | (is (= "hey" (foo 8 4))) 75 | (is (= 123 (foo 100 5)))))) 76 | 77 | (testing "fake with several matchers throws an exception on unmatched args" 78 | (f/with-fakes 79 | (let [foo (fake-fn [[11 22] 1 80 | [8 4] 2])] 81 | (u/is-error-thrown 82 | #"^Unexpected args are passed into fake: \(100 100\).\nSupported args matchers:\n\[11 22\]\n\[8 4\]" 83 | (foo 100 100))))) 84 | 85 | (testing "first matching rule wins" 86 | (f/with-fakes 87 | (let [foo (fake-fn [f/any 1 88 | [2] 2 89 | [3 4] 3])] 90 | (is (= 1 (foo))) 91 | (is (= 1 (foo 2))) 92 | (is (= 1 (foo 3 4)))))) 93 | 94 | (when-not config-required? 95 | (testing "without config fake returns unique values on each call" 96 | (f/with-fakes 97 | (let [foo (fake-fn)] 98 | (is (not= (foo 1 2 3) (foo 1 2 3) (foo 100) (foo)))))) 99 | 100 | (testing "without config fake returns unique values on each call (using explicit context)" 101 | (let [ctx (fc/context) 102 | foo (ctx-fake-fn ctx)] 103 | (is (not= (foo 1 2 3) (foo 1 2 3) (foo 100) (foo))))) 104 | ) 105 | ) -------------------------------------------------------------------------------- /test/unit/were_called_in_order.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.were-called-in-order 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f])) 6 | 7 | (u/deftest+ 8 | "passes when function was called twice" 9 | (f/with-fakes 10 | (let [foo (f/recorded-fake)] 11 | (foo) 12 | (foo) 13 | 14 | (is (f/were-called-in-order foo [])) 15 | (is (f/were-called-in-order 16 | foo [] 17 | foo []))))) 18 | 19 | (u/deftest+ 20 | "throws if function was not called" 21 | (f/with-fakes 22 | (let [foo (f/recorded-fake)] 23 | (u/is-error-thrown 24 | #"^Could not find a call satisfying step #1" 25 | (f/were-called-in-order 26 | foo []))))) 27 | 28 | (u/deftest+ 29 | "throws if other function was not called" 30 | (f/with-fakes 31 | (let [foo (f/recorded-fake) 32 | bar (f/recorded-fake)] 33 | (foo) 34 | 35 | (u/is-error-thrown 36 | #"^Could not find a call satisfying step #2" 37 | (f/were-called-in-order 38 | foo [] 39 | bar []))))) 40 | 41 | (u/deftest+ 42 | "throws if different functions were not called in expected order" 43 | (f/with-fakes 44 | (let [foo (f/recorded-fake) 45 | bar (f/recorded-fake)] 46 | (bar) 47 | (foo) 48 | 49 | (u/is-error-thrown 50 | #"^Could not find a call satisfying step #2" 51 | (f/were-called-in-order 52 | foo [] 53 | bar []))))) 54 | 55 | (u/deftest+ 56 | "throws if function was called less times than expected" 57 | (f/with-fakes 58 | (let [foo (f/recorded-fake)] 59 | (foo) 60 | (foo) 61 | 62 | (u/is-error-thrown 63 | #"^Could not find a call satisfying step #3" 64 | (f/were-called-in-order 65 | foo [] 66 | foo [] 67 | foo []))))) 68 | 69 | (u/deftest+ 70 | "throws if function was not called with specified args" 71 | (f/with-fakes 72 | (let [foo (f/recorded-fake)] 73 | (foo 1 2 3) 74 | 75 | (u/is-error-thrown 76 | #"^Could not find a call satisfying step #1" 77 | (f/were-called-in-order 78 | foo [100 200 300]))))) 79 | 80 | (u/deftest+ 81 | "passes when function was called once" 82 | (f/with-fakes 83 | (let [foo (f/recorded-fake)] 84 | (foo) 85 | (is (f/were-called-in-order foo []))))) 86 | 87 | (u/deftest+ 88 | "(integration) passes with args-matches when three functions were called" 89 | (f/with-fakes 90 | (let [foo (f/recorded-fake) 91 | bar (f/recorded-fake) 92 | baz (f/recorded-fake)] 93 | (foo 1 2 3) 94 | (foo 4) 95 | (bar 100 200) 96 | (baz 300) 97 | (bar 400 500 600) 98 | 99 | (is (f/were-called-in-order 100 | foo [1 2 3] 101 | foo [4])) 102 | 103 | (is (f/were-called-in-order 104 | foo [1 2 3] 105 | bar [400 500 600])) 106 | 107 | (is (f/were-called-in-order 108 | baz [300] 109 | bar [400 500 600])) 110 | 111 | (is (f/were-called-in-order 112 | foo [1 2 3] 113 | foo [(f/arg integer?)] 114 | bar [100 200] 115 | baz [300] 116 | bar [400 500 600]))))) 117 | 118 | (u/deftest+ 119 | "specifies step details on exception" 120 | (f/with-fakes 121 | (let [foo (f/recorded-fake)] 122 | (foo 1 2 3) 123 | 124 | (u/is-error-thrown 125 | #"^Could not find a call satisfying step #1:\nrecorded fake from .*unit/were_called_in_order\.cljc, 121:15\nargs matcher: \[100 \]" 126 | (f/were-called-in-order 127 | foo [100 (f/arg string?)]))))) 128 | 129 | (u/deftest+ 130 | "(regression) correctly reports a step on any args matcher" 131 | (f/with-fakes 132 | (let [foo (f/recorded-fake [[1 2] 3])] 133 | (u/is-error-thrown 134 | #"^Could not find a call satisfying step #1:\nrecorded fake from .*unit/were_called_in_order\.cljc, 132:15\nargs matcher: " 135 | (f/were-called-in-order foo f/any))))) 136 | 137 | (u/deftest+ 138 | "marks all mentioned fakes checked, even on failure" 139 | (f/with-fakes 140 | (let [foo (f/recorded-fake) 141 | bar (f/recorded-fake)] 142 | (u/is-error-thrown 143 | #"^Could not find a call satisfying step #1" 144 | (f/were-called-in-order foo f/any 145 | bar f/any))))) -------------------------------------------------------------------------------- /src/clj_fakes/core.cljc: -------------------------------------------------------------------------------- 1 | (ns clj-fakes.core 2 | "Simpler API for working in implicit dynamic context. 3 | Implements almost the same set of functions as [[clj-fakes.context]]." 4 | (:require [clj-fakes.context :as fc] 5 | #?@(:clj [ 6 | [clj-fakes.macro :as m]])) 7 | #?(:cljs (:require-macros [clj-fakes.core]))) 8 | 9 | ;;;;;;;;;;;;;;;;;;;;;;;; Re-exports for usage convenience 10 | #?(:clj 11 | (defmacro arg [matcher] 12 | `(fc/arg ~matcher))) 13 | 14 | (def any fc/any) 15 | (def cyclically fc/cyclically) 16 | 17 | ;;;;;;;;;;;;;;;;;;;;;;;; Core 18 | (def ^{:doc "You can use this atom in your code but do not alter it directly; instead, always use framework API. 19 | 20 | Also see [[with-fakes]] macro."} 21 | ^:dynamic *context* nil) 22 | 23 | ;;;;;;;;;;;;;;;;;;;;;;;; Macros - with-fakes 24 | (def ^:no-doc ^:dynamic *-probe* nil) 25 | #?(:clj 26 | (defmacro ^:no-doc -check-binding-works 27 | [] 28 | `(do 29 | (fc/-set-var! #'*-probe* nil) 30 | (binding [*-probe* "bound value"]) 31 | (when (some? *-probe*) 32 | (assert false 33 | "with-fakes cannot be used here because binding macro doesn't work correctly. This can happen if with-fakes is used in CLJS go-block (related to https://dev.clojure.org/jira/browse/CLJS-884)."))))) 34 | 35 | #?(:clj 36 | (defmacro with-fakes 37 | "Defines an implicit dynamic [[*context*]] and executes the body in it. 38 | Inside the body you can use the simpler [[clj-fakes.core]] API instead of [[clj-fakes.context]] API. 39 | 40 | The block will automatically unpatch all the patched variables and execute self-tests on exiting. 41 | Self-tests will not be executed if exception was raised inside the body. 42 | Variables are guaranteed to always be unpatched on exiting the block." 43 | [& body] 44 | (let [exception-class (if (m/-cljs-env? &env) 45 | ; ClojureScript 46 | :default 47 | 48 | ; Clojure 49 | Throwable)] 50 | `(do 51 | (-check-binding-works) 52 | 53 | (binding [*context* (fc/context)] 54 | (let [exception-caught?# (atom false)] 55 | (try 56 | ~@body 57 | 58 | (catch ~exception-class e# 59 | (reset! exception-caught?# true) 60 | (throw e#)) 61 | 62 | (finally 63 | (fc/unpatch-all! *context*) 64 | 65 | (when-not @exception-caught?# 66 | (fc/self-test *context*)))))))))) 67 | 68 | (defn with-fakes* 69 | "The function which works similarly to [[with-fakes]] macro. 70 | It defines an implicit dynamic [[*context*]] and 71 | executes function `f` with specified arguments inside the context. 72 | 73 | It is left in mostly for backwards compatibility because previously macro was based on this public function." 74 | [f & args] 75 | (clj-fakes.core/with-fakes (apply f args))) 76 | 77 | ;;;;;;;;;;;;;;;;;;;;;;;; Function fakes 78 | (defn optional-fake 79 | ([] (fc/optional-fake *context*)) 80 | ([config] (fc/optional-fake *context* config))) 81 | 82 | (defn ^:no-doc -position 83 | [f] 84 | (fc/-position *context* f)) 85 | 86 | #?(:clj 87 | (defmacro fake* 88 | [form config] 89 | `(fc/fake* *context* ~form ~config))) 90 | 91 | #?(:clj 92 | (defmacro fake 93 | [config] 94 | `(fake* ~&form ~config))) 95 | 96 | #?(:clj 97 | (defmacro recorded-fake* 98 | ([form] `(fc/recorded-fake* *context* ~form)) 99 | ([form config] `(fc/recorded-fake* *context* ~form ~config)))) 100 | 101 | #?(:clj 102 | (defmacro recorded-fake 103 | ([] `(recorded-fake* ~&form)) 104 | ([config] `(recorded-fake* ~&form ~config)))) 105 | 106 | (defn calls 107 | ([] (fc/calls *context*)) 108 | ([f] (fc/calls *context* f))) 109 | 110 | (defn mark-checked 111 | [f] 112 | (fc/mark-checked *context* f)) 113 | 114 | ;;;;;;;;;;;;;;;;;;;;;;;; Protocol fakes 115 | #?(:clj 116 | (defmacro reify-fake* 117 | [form env & specs] 118 | `(fc/reify-fake* *context* ~form ~env ~@specs))) 119 | 120 | #?(:clj 121 | (defmacro reify-fake 122 | [& specs] 123 | `(reify-fake* ~&form ~&env ~@specs))) 124 | 125 | #?(:clj 126 | (defmacro ^:no-doc reify-fake-debug 127 | "Helper for debugging." 128 | [& specs] 129 | `(fc/-reify-fake-debug* *context* ~&form ~&env ~@specs))) 130 | 131 | #?(:clj 132 | (defmacro reify-nice-fake* 133 | [form env & specs] 134 | `(fc/reify-nice-fake* *context* ~form ~env ~@specs))) 135 | 136 | #?(:clj 137 | (defmacro reify-nice-fake 138 | [& specs] 139 | `(reify-nice-fake* ~&form ~&env ~@specs))) 140 | 141 | (defn method 142 | [obj f] 143 | (fc/method *context* obj f)) 144 | 145 | ;;;;;;;;;;;;;;;;;;;;;;;; Assertions 146 | (defn was-called-once 147 | [f args-matcher] 148 | (fc/was-called-once *context* f args-matcher)) 149 | 150 | (defn was-called 151 | [f args-matcher] 152 | (fc/was-called *context* f args-matcher)) 153 | 154 | (defn was-matched-once 155 | [f args-matcher] 156 | (fc/was-matched-once *context* f args-matcher)) 157 | 158 | (defn was-not-called 159 | [f] 160 | (fc/was-not-called *context* f)) 161 | 162 | (defn were-called-in-order 163 | [& fns-and-matchers] 164 | (apply fc/were-called-in-order *context* fns-and-matchers)) 165 | 166 | ;;;;;;;;;;;;;;;;;;;;;;;; Assertions for protocol methods 167 | (defn method-was-called-once 168 | [f obj args-matcher] 169 | (fc/method-was-called-once *context* f obj args-matcher)) 170 | 171 | (defn method-was-called 172 | [f obj args-matcher] 173 | (fc/method-was-called *context* f obj args-matcher)) 174 | 175 | (defn method-was-matched-once 176 | [f obj args-matcher] 177 | (fc/method-was-matched-once *context* f obj args-matcher)) 178 | 179 | (defn method-was-not-called 180 | [f obj] 181 | (fc/method-was-not-called *context* f obj)) 182 | 183 | (defn methods-were-called-in-order 184 | [& fns-objs-and-matchers] 185 | (apply fc/methods-were-called-in-order *context* fns-objs-and-matchers)) 186 | 187 | ;;;;;;;;;;;;;;;;;;;;;;;; Monkey patching 188 | #?(:clj 189 | (defmacro patch! 190 | [var-expr val] 191 | `(fc/patch! *context* ~var-expr ~val))) 192 | 193 | (defn original-val 194 | [a-var] 195 | (fc/original-val *context* a-var)) 196 | 197 | (defn unpatch! 198 | [a-var] 199 | (fc/unpatch! *context* a-var)) 200 | 201 | (defn unpatch-all! 202 | [] 203 | (fc/unpatch-all! *context*)) -------------------------------------------------------------------------------- /test/unit/positions.cljc: -------------------------------------------------------------------------------- 1 | ; positioning is also partially tested implicitly in tests about self-tests 2 | (ns unit.positions 3 | (:require 4 | [clojure.test :refer [is testing]] 5 | [unit.utils :as u] 6 | [clj-fakes.core :as f] 7 | [clj-fakes.context :as fc] 8 | [unit.fixtures.macros :as m])) 9 | 10 | (def this-file-re #".*unit/positions.cljc$") 11 | 12 | (defn is-pos 13 | [pos expected-line expected-column] 14 | (is (not (nil? (re-find this-file-re (:file pos))))) 15 | (is (= expected-line (:line pos))) 16 | (is (= expected-column (:column pos)))) 17 | 18 | (defn is-fake-has-position 19 | [f expected-line expected-column] 20 | (is-pos (f/-position f) expected-line expected-column)) 21 | 22 | (defn is-fake-has-position-in-context 23 | [ctx f expected-line expected-column] 24 | (is-pos (fc/-position ctx f) expected-line expected-column)) 25 | 26 | (defn test-fake-fn-position-detection 27 | [fake-fn expected-line expected-column 28 | ctx-fake-fn ctx-expected-line ctx-expected-column] 29 | (testing "fake position can be determined in implicit context" 30 | (f/with-fakes 31 | (let [foo (fake-fn [f/any nil])] 32 | (foo) ; call to suppress an exception from self-test 33 | (is-fake-has-position foo expected-line expected-column)))) 34 | 35 | (testing "fake position can be determined in explicit context" 36 | (let [ctx (fc/context) 37 | foo (ctx-fake-fn ctx [f/any nil])] 38 | (is-fake-has-position-in-context ctx foo ctx-expected-line ctx-expected-column)))) 39 | 40 | (u/deftest+ 41 | "position detection for fake" 42 | (test-fake-fn-position-detection 43 | ; we can't pass a macro into a function so let's wrap it into a func 44 | (fn [config] (f/fake config)) 45 | 44 18 46 | (fn [ctx config] (fc/fake ctx config)) 47 | 46 22)) 48 | 49 | (u/deftest+ 50 | "position detection for recorded fake with config" 51 | (test-fake-fn-position-detection 52 | ; we can't pass a macro into a function so let's wrap it into a func 53 | (fn [config] 54 | (let [fake-fn (f/recorded-fake config)] 55 | (f/mark-checked fake-fn) 56 | fake-fn)) 57 | 54 21 58 | (fn [ctx config] (fc/recorded-fake ctx config)) 59 | 58 22)) 60 | 61 | (u/deftest+ 62 | "position detection for recorded fake without config" 63 | (test-fake-fn-position-detection 64 | ; we can't pass a macro into a function so let's wrap it into a func 65 | (fn [_config_] 66 | (let [fake-fn (f/recorded-fake)] 67 | ; supress self-test warnings 68 | (f/mark-checked fake-fn) 69 | fake-fn)) 70 | 66 21 71 | (fn [ctx _config_] (fc/recorded-fake ctx)) 72 | 71 24)) 73 | 74 | (u/deftest+ 75 | "fake can be reused in a custom macro without losing ability to detect position" 76 | (test-fake-fn-position-detection 77 | ; we can't pass a macro into a function so let's wrap it into a func 78 | (fn [config] (m/my-fake config)) 79 | 78 18 80 | (fn [ctx config] (m/ctx-my-fake ctx config)) 81 | 80 22)) 82 | 83 | (u/deftest+ 84 | "recorded-fake with config can be reused in a custom macro without losing ability to detect position" 85 | (test-fake-fn-position-detection 86 | ; we can't pass a macro into a function so let's wrap it into a func 87 | (fn [config] 88 | (let [fake-fn (m/my-recorded-fake config)] 89 | (f/mark-checked fake-fn) 90 | fake-fn)) 91 | 88 21 92 | (fn [ctx config] (m/ctx-my-recorded-fake ctx config)) 93 | 92 22)) 94 | 95 | (u/deftest+ 96 | "recorded-fake without config can be reused in a custom macro without losing ability to detect position" 97 | (test-fake-fn-position-detection 98 | ; we can't pass a macro into a function so let's wrap it into a func 99 | (fn [_config_] 100 | (let [fake-fn (m/my-recorded-fake)] 101 | (f/mark-checked fake-fn) 102 | fake-fn)) 103 | 100 21 104 | (fn [ctx _config_] (m/ctx-my-recorded-fake ctx)) 105 | 104 24)) 106 | 107 | (defprotocol LocalProtocol 108 | (bar [_])) 109 | 110 | (u/deftest+ 111 | "position can be determined in reify-fake" 112 | (testing "recorded fake with config, implicit context" 113 | (f/with-fakes 114 | (let [foo (f/reify-fake LocalProtocol 115 | (bar :recorded-fake [f/any nil]))] 116 | ; call to suppress self-test warning 117 | (f/mark-checked (f/method foo bar)) 118 | (is-fake-has-position (f/method foo bar) 114 17)))) 119 | 120 | (testing "recorded fake without config, implicit context" 121 | (f/with-fakes 122 | (let [foo (f/reify-fake LocalProtocol 123 | (bar :recorded-fake))] 124 | ; call to suppress self-test warning 125 | (f/mark-checked (f/method foo bar)) 126 | (is-fake-has-position (f/method foo bar) 122 17)))) 127 | 128 | (testing "recorded fake with config, explicit context" 129 | (f/with-fakes 130 | (let [ctx (fc/context) 131 | foo (fc/reify-fake ctx LocalProtocol 132 | (bar :recorded-fake [f/any nil]))] 133 | (is-fake-has-position-in-context ctx (fc/method ctx foo bar) 131 17)))) 134 | 135 | (testing "recorded fake without config, explicit context" 136 | (f/with-fakes 137 | (let [ctx (fc/context) 138 | foo (fc/reify-fake ctx LocalProtocol 139 | (bar :recorded-fake))] 140 | (is-fake-has-position-in-context ctx (fc/method ctx foo bar) 138 17))))) 141 | 142 | (u/deftest+ 143 | "position can be determined in reify-fake if it's used from a custom macro" 144 | (testing "recorded fake with config, implicit context" 145 | (f/with-fakes 146 | (let [foo (m/my-reify-fake LocalProtocol 147 | (bar :recorded-fake [f/any nil]))] 148 | ; call to suppress self-test warning 149 | (f/mark-checked (f/method foo bar)) 150 | (is-fake-has-position (f/method foo bar) 146 17)))) 151 | 152 | (testing "recorded fake without config, implicit context" 153 | (f/with-fakes 154 | (let [foo (m/my-reify-fake LocalProtocol 155 | (bar :recorded-fake))] 156 | ; call to suppress self-test warning 157 | (f/mark-checked (f/method foo bar)) 158 | (is-fake-has-position (f/method foo bar) 154 17)))) 159 | 160 | (testing "recorded fake with config, explicit context" 161 | (f/with-fakes 162 | (let [ctx (fc/context) 163 | foo (m/ctx-my-reify-fake ctx LocalProtocol 164 | (bar :recorded-fake [f/any nil]))] 165 | (is-fake-has-position-in-context ctx (fc/method ctx foo bar) 163 17)))) 166 | 167 | (testing "recorded fake without config, explicit context" 168 | (f/with-fakes 169 | (let [ctx (fc/context) 170 | foo (m/ctx-my-reify-fake ctx LocalProtocol 171 | (bar :recorded-fake))] 172 | (is-fake-has-position-in-context ctx (fc/method ctx foo bar) 170 17))))) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **clj-fakes** is an isolation framework for Clojure/ClojureScript that makes creating [test doubles](https://en.wikipedia.org/wiki/Test_double) much easier. 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/clj-fakes.svg)](https://clojars.org/clj-fakes) 4 | [![Gitter](https://img.shields.io/gitter/room/metametadata/clj-fakes.svg?maxAge=2592000?style=plastic)](https://gitter.im/metametadata/clj-fakes) 5 | 6 | # Features 7 | * All test doubles are named "fakes" to simplify the terminology. 8 | * Fakes can be created for: 9 | * functions 10 | * instances of protocols and Java interfaces 11 | * "Nice" and "strict" protocol fakes are supported. 12 | * Monkey patching is supported to fake implicit dependencies. 13 | * Several functions are provided for asserting recorded calls. 14 | * Self-testing: automatically checks for unused fakes. 15 | * Informative error messages. 16 | * Test runner agnostic. 17 | * Arrange-Act-Assert style testing. 18 | 19 | # Installation 20 | Requirements: Clojure 1.7.0+ and/or ClojureScript 1.10.238+. 21 | 22 | Add this to your dependencies: 23 | 24 | ```clj 25 | [clj-fakes "0.12.0"] 26 | ``` 27 | 28 | Require framework namespace in your unit test source file: 29 | 30 | ```clj 31 | (ns unit.example 32 | (:require 33 | [clj-fakes.core :as f] 34 | ; and/or: 35 | [clj-fakes.context :as fc])) 36 | ``` 37 | 38 | # Cheat Sheet 39 | 40 | ### Creating Faking Context 41 | 42 | Explicit context: 43 | 44 | ```clj 45 | (let [ctx (fc/context)] 46 | ; use clj-fakes.context API here 47 | ) 48 | ``` 49 | 50 | Implicit context: 51 | 52 | ```clj 53 | (f/with-fakes 54 | ; use clj-fakes.core API here 55 | ) 56 | ; on exit block will automatically unpatch all patched vars and execute self-tests 57 | ``` 58 | 59 | All the following examples are assumed to be used inside an implicit context. 60 | 61 | ### Stubbing 62 | 63 | #### Function Stub 64 | 65 | ```clj 66 | (let [foo (f/fake [[1 2] "foo" 67 | [3 4 5] "bar"])] 68 | (foo 1 2) ; => "foo" 69 | (foo 3 4 5) ; => "bar" 70 | (foo 100 200)) ; => raises "Unexpected args are passed into fake: (100 200) ..." 71 | ``` 72 | 73 | #### Method Stub 74 | 75 | ```clj 76 | (let [cow (f/reify-fake p/AnimalProtocol 77 | (sleep :fake [[] "zzz"]))] 78 | (p/sleep cow) ; => "zzz" 79 | (p/speak cow)) ; => undefined method exception 80 | ``` 81 | 82 | #### Nice Method Stub 83 | 84 | ```clj 85 | (let [cow (f/reify-nice-fake p/AnimalProtocol)] 86 | (p/sleep cow) ; => FakeReturnValue 87 | (p/speak cow)) ; => FakeReturnValue 88 | ``` 89 | 90 | ### Mocking 91 | 92 | #### Function Mock 93 | 94 | ```clj 95 | (let [foo (f/recorded-fake [[(f/arg integer?) (f/arg integer?)] #(+ %1 %2)]) 96 | bar (f/recorded-fake [[(f/arg integer?) (f/arg integer?)] #(* %1 %2)])] 97 | (foo 1 2) 98 | (bar 5 6) 99 | (foo 7 8) 100 | 101 | (f/calls foo) 102 | ; => [{:args [1 2] :return-value 3} 103 | ; {:args [7 8] :return-value 15}] 104 | 105 | (f/calls) 106 | ; => [[foo {:args [1 2] :return-value 3}] 107 | ; [bar {:args [5 6] :return-value 30}] 108 | ; [foo {:args [7 8] :return-value 15}]] 109 | ) 110 | ``` 111 | 112 | #### Method Mock 113 | 114 | ```clj 115 | (let [cow (f/reify-fake p/AnimalProtocol 116 | (speak :recorded-fake [f/any "moo"]))] 117 | (p/speak cow) 118 | 119 | (f/calls (f/method cow p/speak))) ; => [{:args ..., :return-value moo}] 120 | ``` 121 | 122 | ### Assertions 123 | 124 | These functions return true or raise an exception and can be used only with recorded fakes. 125 | 126 | #### Strictly One Call 127 | 128 | ```clj 129 | (f/was-called-once foo [1 2]) 130 | 131 | (f/method-was-called-once p/speak cow ["Bob"]) 132 | ``` 133 | 134 | #### At Least One Call 135 | 136 | ```clj 137 | (f/was-called foo [1 2]) 138 | 139 | (f/method-was-called p/speak cow ["Bob"]) 140 | ``` 141 | 142 | #### Strictly One Call Matched The Provided Args Matcher 143 | 144 | ```clj 145 | (f/was-matched-once foo [1 2]) 146 | 147 | (f/method-was-matched-once p/speak cow ["Bob"]) 148 | ``` 149 | 150 | #### No Calls 151 | 152 | ```clj 153 | (f/was-not-called foo) 154 | 155 | (f/method-was-not-called p/speak cow) 156 | ``` 157 | 158 | #### Calls In The Specified Order 159 | 160 | ```clj 161 | (f/were-called-in-order 162 | foo [1 2 3] 163 | foo [(f/arg integer?)] 164 | bar [100 200] 165 | baz [300]) 166 | 167 | (f/methods-were-called-in-order 168 | p/speak cow [] 169 | p/sleep cow [] 170 | p/eat dog ["dog food" "water"] 171 | p/speak cow ["Bob"]) 172 | ``` 173 | 174 | ### Monkey Patching 175 | 176 | **Caution**: this feature is not thread-safe. 177 | Strongly consider avoiding it in Clojure code if you plan to someday run your tests concurrently. 178 | 179 | #### Patch Function With Stub 180 | 181 | ```clj 182 | (f/with-fakes 183 | (f/patch! #'funcs/sum (f/fake [[1 2] "foo" 184 | [3 4] "bar"])) 185 | (funcs/sum 1 2) ; => "foo" 186 | (funcs/sum 3 4)) ; => "bar" 187 | 188 | ; patching is reverted on exiting with-fakes block 189 | (funcs/sum 1 2) ; => 3 190 | ``` 191 | 192 | #### Patch To Spy 193 | 194 | ```clj 195 | (f/patch! #'funcs/sum (f/recorded-fake [f/any funcs/sum])) 196 | (funcs/sum 1 2) ; => 3 197 | (f/was-called funcs/sum [1 2]) ; => true 198 | ``` 199 | 200 | ### Self-tests 201 | 202 | ```clj 203 | (f/with-fakes 204 | (f/fake [f/any nil])) 205 | ; => raises "Self-test: no call detected for: non-optional fake ..." 206 | 207 | (f/with-fakes 208 | (f/recorded-fake)) 209 | ; => raises "Self-test: no check performed on: recorded fake ..." 210 | ``` 211 | 212 | # Documentation 213 | More documentation can be found at [the project site](http://metametadata.github.io/clj-fakes/): 214 | 215 | * [Quickstart](http://metametadata.github.io/clj-fakes/quickstart/) 216 | * [User Guide](http://metametadata.github.io/clj-fakes/user-guide/) 217 | * [API Reference](http://metametadata.github.io/clj-fakes/api/) 218 | * [Developer Guide](http://metametadata.github.io/clj-fakes/dev-guide/) 219 | 220 | # References 221 | The API was mainly inspired by [jMock](http://www.jmock.org/) and 222 | [unittest.mock](https://docs.python.org/3/library/unittest.mock.html) frameworks with 223 | design decisions loosely based on the 224 | ["Fifteen things I look for in an Isolation framework" by Roy Osherove](http://osherove.com/blog/2013/11/23/fifteen-things-i-look-for-in-an-isolation-framework.html). 225 | 226 | Some alternative frameworks with isolation capabilities: 227 | 228 | * [bond](https://github.com/circleci/bond) 229 | * [clj-mock](https://github.com/EchoTeam/clj-mock) 230 | * [conjure](https://github.com/amitrathore/conjure) 231 | * [Midje](https://github.com/marick/Midje) 232 | * [shrubbery](https://github.com/bguthrie/shrubbery) 233 | * [speclj](https://github.com/slagyr/speclj) 234 | * [Untangled Spec](https://github.com/untangled-web/untangled-spec) 235 | 236 | Also take at look at the article 237 | ["Isolating External Dependencies in Clojure" by Joseph Wilk](http://blog.josephwilk.net/clojure/isolating-external-dependencies-in-clojure.html). 238 | 239 | For more detailed information about unit testing, TDD and test double patterns I'd recommend the books below: 240 | 241 | * "Test Driven Development: By Example" by Kent Beck 242 | * "Growing Object-Oriented Software, Guided by Tests" by Steve Freeman and Nat Pryce [[site](http://www.growing-object-oriented-software.com/)] 243 | * "xUnit Test Patterns: Refactoring Test Code" by Gerard Meszaros [[site](http://xunitpatterns.com/)] 244 | 245 | # License 246 | Copyright © 2015 Yuri Govorushchenko. 247 | 248 | Released under an MIT license. 249 | -------------------------------------------------------------------------------- /test/unit/patch.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.patch 2 | (:require 3 | [clojure.test :refer [is testing #?(:cljs async)]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [clj-fakes.context :as fc] 7 | [unit.fixtures.functions :as funcs])) 8 | 9 | (def my-var1 111) 10 | (def my-var2 222) 11 | 12 | (defrecord MyRecord [field]) 13 | 14 | (u/deftest+ 15 | "var can be patched inside the context" 16 | (f/with-fakes 17 | (is (not= 200 my-var1)) 18 | (f/patch! #'my-var1 200) 19 | (is (= 200 my-var1)))) 20 | 21 | (u/deftest+ 22 | "patched var is recovered on exiting mocking context" 23 | (let [original-val my-var1] 24 | (f/with-fakes 25 | (f/patch! #'my-var1 200)) 26 | 27 | (is (= original-val my-var1)))) 28 | 29 | (u/deftest+ 30 | "fully qualified single patched var is recovered on exiting mocking context" 31 | (let [original-val my-var1] 32 | (f/with-fakes 33 | (f/patch! #'unit.patch/my-var1 200)) 34 | 35 | (is (= original-val my-var1)))) 36 | 37 | (u/deftest+ 38 | "patched var is recovered on exiting mocking context because of exception" 39 | (let [original-val my-var1] 40 | (u/is-error-thrown 41 | #"expected exception" 42 | (try 43 | (f/with-fakes 44 | (f/patch! #'my-var1 200) 45 | (throw (ex-info "expected exception" {}))))) 46 | 47 | (is (= original-val my-var1)))) 48 | 49 | (u/deftest+ 50 | "two patched vars are recovered on exiting mocking context" 51 | (let [original-val1 my-var1 52 | original-val2 my-var2] 53 | (f/with-fakes 54 | (f/patch! #'my-var1 100) 55 | (f/patch! #'my-var2 200)) 56 | 57 | (is (= original-val1 my-var1)) 58 | (is (= original-val2 my-var2)))) 59 | 60 | (u/deftest+ 61 | "var can be patched more than once and be recovered" 62 | (let [original-val my-var1] 63 | (f/with-fakes 64 | (f/patch! #'my-var1 200) 65 | (f/patch! #'my-var1 300) 66 | 67 | (is (= 300 my-var1))) 68 | 69 | (is (= original-val my-var1)))) 70 | 71 | (u/deftest+ 72 | "function can be patched inside the context" 73 | (f/with-fakes 74 | (is (not= 123 (funcs/sum 2 3))) 75 | (f/patch! #'funcs/sum (constantly 123)) 76 | (is (= 123 (funcs/sum 2 3))))) 77 | 78 | (u/deftest+ 79 | "println can be patched (this test will fail in Clojure 1.8 with enabled direct linking)" 80 | (f/with-fakes 81 | (f/patch! #'println (constantly 123)) 82 | (is (= 123 (println "YOU SHOULDN'T SEE IT"))))) 83 | 84 | (u/deftest+ 85 | "variadic function can be patched with non-variadic function" 86 | (f/with-fakes 87 | (f/patch! #'funcs/variadic (constantly 200)) 88 | 89 | (is (= 200 (funcs/variadic))) 90 | (is (= 200 (funcs/variadic 1))) 91 | (is (= 200 (funcs/variadic 1 2))) 92 | (is (= 200 (funcs/variadic 1 2 3 4 5 6 7))))) 93 | 94 | (u/deftest+ 95 | "variadic function can be patched with variadic function" 96 | (f/with-fakes 97 | (f/patch! #'funcs/variadic (fn my-variadic 98 | ([] 0) 99 | ([_] 1) 100 | ([_ _] 2) 101 | ([_ _ _] 3) 102 | ([_ _ _ & _] :etc))) 103 | 104 | (is (= 0 (funcs/variadic))) 105 | (is (= 1 (funcs/variadic 1))) 106 | (is (= 2 (funcs/variadic 1 2))) 107 | (is (= 3 (funcs/variadic 1 2 3))) 108 | (is (= :etc (funcs/variadic 1 2 3 4 5 6 7))))) 109 | 110 | (u/deftest+ 111 | "non-variadic function can be patched with recursive variadic function which calls original function" 112 | (f/with-fakes 113 | (let [original-sum funcs/sum] 114 | (f/patch! #'funcs/sum (fn my-variadic 115 | ([] 0) 116 | ([x] (original-sum 0 x)) 117 | ([x y] ((f/original-val #'funcs/sum) x y)) 118 | ([x y z] (->> (my-variadic x y) 119 | ((f/original-val #'funcs/sum) z))) 120 | ([x y z & etc] 121 | ((f/original-val #'funcs/sum) 122 | (my-variadic x y z) 123 | (apply my-variadic etc))))) 124 | 125 | ; alias is created to get rid of "WARNING: Wrong number of args (...) passed to unit.fixtures.functions/sum" 126 | (let [new-sum funcs/sum] 127 | (is (= 0 (new-sum))) 128 | (is (= 100 (new-sum 100))) 129 | (is (= 7 (new-sum 3 4))) 130 | (is (= 10 (new-sum 1 2 3 4))) 131 | (is (= 15 (new-sum 1 2 3 4 5))) 132 | (is (= 21 (new-sum 1 2 3 4 5 6))) 133 | (is (= 120 (new-sum 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15))))))) 134 | 135 | ; TODO: fails in CLJS - see https://github.com/metametadata/clj-fakes/issues/3 136 | #?(:clj 137 | (u/deftest+ 138 | "variadic function can be patched with non-variadic function which calls original function" 139 | (f/with-fakes 140 | (f/patch! #'funcs/variadic (fn my-sum 141 | [x] 142 | ((f/original-val #'funcs/variadic) x))) 143 | 144 | (is (= "[a]" (funcs/variadic 100)))))) 145 | 146 | ; TODO: fails in CLJS - see https://github.com/metametadata/clj-fakes/issues/3 147 | #?(:clj 148 | (u/deftest+ 149 | "variadic function can be patched with variadic function which calls original function" 150 | (f/with-fakes 151 | (let [original-variadic funcs/variadic] 152 | (f/patch! #'funcs/variadic (fn my-variadic 153 | ([] (original-variadic)) 154 | ([a] ((f/original-val #'funcs/variadic) a)) 155 | ([_ _] 2) 156 | ([_ _ _] 3) 157 | ([_ _ _ & _] :etc))) 158 | 159 | (is (= "[]" (funcs/variadic))) 160 | (is (= "[a]" (funcs/variadic 1))) 161 | (is (= 2 (funcs/variadic 1 2))) 162 | (is (= 3 (funcs/variadic 1 2 3))) 163 | (is (= :etc (funcs/variadic 1 2 3 4 5 6 7))))))) 164 | 165 | (u/deftest+ 166 | "multimethod can be patched" 167 | (f/with-fakes 168 | (f/patch! #'funcs/fib (constantly 200)) 169 | (is (= [200 200 200 200 200] (map funcs/fib (range 5)))))) 170 | 171 | (u/deftest+ 172 | "var can be patched with a fake" 173 | (f/with-fakes 174 | (f/patch! #'funcs/sum (f/fake [[1 2] "foo" 175 | [3 4] "bar"])) 176 | (is (= "foo" (funcs/sum 1 2))) 177 | (is (= "bar" (funcs/sum 3 4))))) 178 | 179 | (u/deftest+ 180 | "var can be patched with a recorded fake" 181 | (f/with-fakes 182 | (f/patch! #'funcs/sum (f/recorded-fake [[(f/arg integer?) (f/arg integer?)] #(* %1 %2)])) 183 | 184 | (is (= 2 (funcs/sum 1 2))) 185 | (is (= 12 (funcs/sum 3 4))) 186 | 187 | (is (f/was-called funcs/sum [1 2])) 188 | (is (f/was-called funcs/sum [3 4])))) 189 | 190 | (u/deftest+ 191 | "record instantiation using -> can be patched" 192 | (f/with-fakes 193 | (f/patch! #'->MyRecord (constantly 123)) 194 | (is (= 123 (->MyRecord {:field 111}))))) 195 | 196 | (u/deftest+ 197 | "record instantiation using map-> can be patched" 198 | (f/with-fakes 199 | (f/patch! #'map->MyRecord (constantly 123)) 200 | (is (= 123 (map->MyRecord {:field 111}))))) 201 | 202 | #?(:cljs 203 | (u/deftest+ 204 | "var stays patched in setTimeout context" 205 | (async done 206 | (let [ctx (fc/context)] 207 | (is (not= 200 my-var1)) 208 | (fc/patch! ctx #'my-var1 200) 209 | (.setTimeout js/window 210 | #(do 211 | ; assert 212 | (is (= 200 my-var1)) 213 | 214 | ; tear down 215 | (fc/unpatch! ctx #'my-var1) 216 | (is (not= 200 my-var1)) 217 | (done)) 218 | 10))))) -------------------------------------------------------------------------------- /test/unit/reify_nice_fake.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.reify-nice-fake 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [clj-fakes.context :as fc] 7 | [unit.fixtures.protocols :as p :refer [AnimalProtocol]])) 8 | 9 | (defprotocol LocalProtocol 10 | (bar [this] [this x y]) 11 | (baz [this x]) 12 | (qux [this x y z])) 13 | 14 | (defn is-faked 15 | [method & args] 16 | ; strings are compared instead of values, because, presumably, 'lein test-refresh' plugin incorrectly reloads deftype 17 | ; and tests start failing on every change to contex.cljc 18 | (is (= #?(:clj "clj_fakes.context.FakeReturnValue" 19 | :cljs "clj-fakes.context/FakeReturnValue") 20 | (pr-str (type (apply method args))))) 21 | 22 | (is (not= (apply method args) 23 | (apply method args)))) 24 | 25 | (u/deftest+ 26 | "methods from same-namespace-protocol can be automatically faked" 27 | (f/with-fakes 28 | (let [foo (f/reify-nice-fake LocalProtocol)] 29 | (is-faked bar foo) 30 | (is-faked bar foo 1 2) 31 | (is-faked baz foo 100) 32 | (is-faked qux foo 1 2 3)))) 33 | 34 | (u/deftest+ 35 | "method from fully-qualified protocol can be automatically faked" 36 | (f/with-fakes 37 | (let [cow (f/reify-nice-fake p/AnimalProtocol)] 38 | (is-faked p/speak cow)))) 39 | 40 | (u/deftest+ 41 | "method from refered protocol can be automatically faked" 42 | (f/with-fakes 43 | (let [cow (f/reify-nice-fake AnimalProtocol)] 44 | (is-faked p/speak cow)))) 45 | 46 | (u/deftest+ 47 | "works in explicit context" 48 | (let [ctx (fc/context) 49 | cow (fc/reify-nice-fake ctx AnimalProtocol)] 50 | (is-faked p/speak cow))) 51 | ; 52 | ;(u/-deftest 53 | ; "method with arglists can be automatically reified even if one of arglists is faked explicitly" 54 | ; (f/with-fakes 55 | ; (let [foo (f/reify-nice-fake LocalProtocol 56 | ; [bar :optional-fake [f/any "bar"]])] 57 | ; (is (= "bar" (bar foo))) 58 | ; (is-faked bar foo 1 2)))) 59 | 60 | (u/deftest+ 61 | "several protocols can be automatically reified" 62 | (f/with-fakes 63 | (let [cow (f/reify-nice-fake p/AnimalProtocol 64 | p/FileProtocol)] 65 | (is-faked p/speak cow) 66 | (is-faked p/speak cow 1) 67 | (is-faked p/speak cow 1 2) 68 | (is-faked p/eat cow 100 200) 69 | (is-faked p/sleep cow) 70 | 71 | (is-faked p/save cow) 72 | (is-faked p/scan cow)))) 73 | 74 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Java interface 75 | #?(:clj 76 | (u/deftest+ 77 | "IFn cannot be automatically reified" 78 | (u/is-exception-thrown 79 | java.lang.AbstractMethodError 80 | "n/a" 81 | #"" 82 | (f/with-fakes 83 | (let [foo (f/reify-nice-fake clojure.lang.IFn)] 84 | (foo 1 2 3 4)))))) 85 | 86 | #?(:clj 87 | (u/deftest+ 88 | "java.lang.CharSequence can be explicitly reified alongside automatically reified protocol" 89 | (f/with-fakes 90 | (let [foo (f/reify-nice-fake 91 | java.lang.CharSequence 92 | (charAt :fake [[100] \a]) 93 | 94 | p/FileProtocol)] 95 | (is (= \a (.charAt foo 100))) 96 | 97 | (is-faked p/save foo) 98 | (is-faked p/scan foo))))) 99 | 100 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Object 101 | #?(:cljs 102 | (u/deftest+ 103 | "Object can be reified with a new optional-fake method" 104 | (f/with-fakes 105 | (let [foo (f/reify-nice-fake Object 106 | (new-method1 [] :optional-fake) 107 | (new-method2 [x y] :optional-fake [[f/any f/any] #(+ %2 %3)]) 108 | (new-method3 [x y z] :optional-fake [f/any "bar"]))] 109 | (is (instance? fc/FakeReturnValue (.new-method1 foo))) 110 | 111 | (is (= 5 (.new-method2 foo 2 3))) 112 | 113 | (is (= "bar" (.new-method3 foo))) 114 | (is (= "bar" (.new-method3 foo 1))) 115 | (is (= "bar" (.new-method3 foo 1 2))) 116 | (is (= "bar" (.new-method3 foo 1 2 3))))))) 117 | 118 | #?(:cljs 119 | (u/deftest+ 120 | "Object can be reified with a new fake method" 121 | (f/with-fakes 122 | (let [foo (f/reify-nice-fake Object 123 | (new-method1 [] :fake [[] "bla"]) 124 | (new-method2 [x y] :fake [[f/any f/any] #(+ %2 %3)]) 125 | (new-method3 [x y z] :fake [f/any "bar"]))] 126 | (is (= "bla" (.new-method1 foo))) 127 | 128 | (is (= 5 (.new-method2 foo 2 3))) 129 | 130 | (is (= "bar" (.new-method3 foo))) 131 | (is (= "bar" (.new-method3 foo 1))) 132 | (is (= "bar" (.new-method3 foo 1 2))) 133 | (is (= "bar" (.new-method3 foo 1 2 3))))))) 134 | 135 | #?(:cljs 136 | (u/deftest+ 137 | "Object can be reified with a new recorded fake method" 138 | (f/with-fakes 139 | (let [foo (f/reify-nice-fake Object 140 | (new-method1 [x] :recorded-fake) 141 | (new-method2 [x y] :recorded-fake [[f/any f/any] #(+ %2 %3)]))] 142 | (is (instance? fc/FakeReturnValue (.new-method1 foo 777))) 143 | (is (= 5 (.new-method2 foo 2 3))) 144 | 145 | (is (f/method-was-called "new-method1" foo [777])) 146 | (is (f/method-was-called "new-method2" foo [2 3])))))) 147 | 148 | (u/deftest+ 149 | "Object/toString cannot be automatically reified" 150 | (f/with-fakes 151 | (let [foo (f/reify-nice-fake Object)] 152 | (is (not (instance? clj_fakes.context.FakeReturnValue (.toString foo))))))) 153 | 154 | #?(:cljs 155 | (u/deftest+ 156 | "Object/toString can be faked" 157 | (f/with-fakes 158 | (let [foo (f/reify-nice-fake Object 159 | (toString [] :recorded-fake [[] "bla"]))] 160 | (is (= "bla" (str foo))) 161 | (is (f/method-was-called "toString" foo [])))))) 162 | 163 | #?(:clj 164 | (u/deftest+ 165 | "Object/toString can be faked" 166 | (f/with-fakes 167 | (let [foo (f/reify-nice-fake Object 168 | (toString :recorded-fake [[] "bla"]))] 169 | (is (= "bla" (str foo))) 170 | (is (f/method-was-called "toString" foo [])))))) 171 | 172 | #?(:clj 173 | (u/deftest+ 174 | "java.lang.Object is also supported" 175 | (f/with-fakes 176 | (let [foo (f/reify-nice-fake java.lang.Object 177 | (toString :fake [[] "bla"]))] 178 | (is (= "bla" (str foo))))))) 179 | 180 | #?(:clj 181 | (u/deftest+ 182 | "Object/toString can be explicitly reified alongside automatically reified protocol" 183 | (f/with-fakes 184 | (let [foo (f/reify-nice-fake Object 185 | (toString :recorded-fake [[] "bla"]) 186 | 187 | p/FileProtocol)] 188 | (is (= "bla" (str foo))) 189 | (is (f/method-was-called "toString" foo [])) 190 | 191 | (is-faked p/save foo) 192 | (is-faked p/scan foo))))) 193 | 194 | #?(:cljs 195 | (u/deftest+ 196 | "Object/toString can be explicitly reified alongside automatically reified protocol" 197 | (f/with-fakes 198 | (let [foo (f/reify-nice-fake Object 199 | (toString [] :recorded-fake [[] "bla"]) 200 | 201 | p/FileProtocol)] 202 | (is (= "bla" (str foo))) 203 | (is (f/method-was-called "toString" foo [])) 204 | 205 | (is-faked p/save foo) 206 | (is-faked p/scan foo))))) 207 | 208 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; integration 209 | (u/deftest+ 210 | "several protocols can be automatically reified and be partially explicitly faked" 211 | (f/with-fakes 212 | (let [cow (f/reify-nice-fake p/AnimalProtocol 213 | (sleep :recorded-fake [[] "zzz"]) 214 | 215 | p/FileProtocol 216 | (scan :recorded-fake))] 217 | 218 | ; AnimalProtocol 219 | (is-faked p/speak cow) 220 | (is-faked p/speak cow 1) 221 | (is-faked p/speak cow 2 3) 222 | (is-faked p/eat cow 100 200) 223 | (is (= "zzz" (p/sleep cow))) 224 | 225 | ; FileProtocol 226 | (is-faked p/save cow) 227 | (p/scan cow) 228 | 229 | ; AnimalProtocol 230 | (f/method-was-called p/sleep cow []) 231 | 232 | ; FileProtocol 233 | (f/method-was-called p/scan cow [])))) -------------------------------------------------------------------------------- /test/unit/reify_fake.cljc: -------------------------------------------------------------------------------- 1 | (ns unit.reify-fake 2 | (:require 3 | [clojure.test :refer [is testing]] 4 | [unit.utils :as u] 5 | [clj-fakes.core :as f] 6 | [clj-fakes.context :as fc] 7 | [unit.fixtures.protocols :as p :refer [AnimalProtocol]]) 8 | #?(:clj 9 | (:import [interop InterfaceFixture] 10 | (java.util Arrays) 11 | (java.net.http WebSocket)))) 12 | 13 | (defprotocol LocalProtocol 14 | (bar [this])) 15 | 16 | ;;;;;;;;;;;;;;;;;;;;;;;;;; optional-fake 17 | (u/deftest+ 18 | "simplest method from same-namespace-protocol can be an optional fake" 19 | (f/with-fakes 20 | (let [foo (f/reify-fake LocalProtocol 21 | (bar :optional-fake [f/any "baz"]))] 22 | (is (= "baz" (bar foo)))))) 23 | 24 | (u/deftest+ 25 | "simplest method from fully-qualified protocol can be an optional fake" 26 | (f/with-fakes 27 | (let [cow (f/reify-fake p/AnimalProtocol 28 | (speak :optional-fake [f/any "moo"]))] 29 | (is (= "moo" (p/speak cow)))))) 30 | 31 | (u/deftest+ 32 | "simplest method from refered protocol can be an optional fake" 33 | (f/with-fakes 34 | (let [cow (f/reify-fake AnimalProtocol 35 | (speak :optional-fake [f/any "moo"]))] 36 | (is (= "moo" (p/speak cow)))))) 37 | 38 | (u/deftest+ 39 | "method with an arg can be an optional fake" 40 | (f/with-fakes 41 | (let [cow (f/reify-fake p/AnimalProtocol 42 | (eat :optional-fake [f/any "om-nom"]))] 43 | (is (= "om-nom" (p/eat cow :grass :water)))))) 44 | 45 | (u/deftest+ 46 | "method args are passed to a fake on call" 47 | (f/with-fakes 48 | (let [cow (f/reify-fake p/AnimalProtocol 49 | (eat :optional-fake [[f/any f/any] #(vector %1 %2 %3)]))] 50 | (is (= [cow "grass" "water"] (p/eat cow "grass" "water")))))) 51 | 52 | (u/deftest+ 53 | "args matchers get correct args on call" 54 | (f/with-fakes 55 | (let [cow (f/reify-fake p/AnimalProtocol 56 | (eat :optional-fake [["grass" "water"] "ate as usual" 57 | ["banana" nil] "ate a banana" 58 | ["" "juice"] "drank some juice"]))] 59 | (is (= "ate as usual" (p/eat cow "grass" "water"))) 60 | (is (= "drank some juice" (p/eat cow "" "juice"))) 61 | (is (= "ate a banana" (p/eat cow "banana" nil)))))) 62 | 63 | (u/deftest+ 64 | "several methods can be reified" 65 | (f/with-fakes 66 | (let [cow (f/reify-fake p/AnimalProtocol 67 | (speak :optional-fake [f/any "moo"]) 68 | (eat :optional-fake [[1 2] "ate"]))] 69 | (is (= "moo" (p/speak cow))) 70 | (is (= "ate" (p/eat cow 1 2)))))) 71 | 72 | (u/deftest+ 73 | "multiple method arglists are supported" 74 | (f/with-fakes 75 | (let [cow (f/reify-fake p/AnimalProtocol 76 | (speak :optional-fake [[] "moo" 77 | [f/any] #(str "moo, " %2) 78 | [f/any f/any] #(str "moo, " %2 " and " %3)]))] 79 | (is (= "moo, User" (p/speak cow "User"))) 80 | (is (= "moo" (p/speak cow))) 81 | (is (= "moo, Bob and Alice" (p/speak cow "Bob" "Alice")))))) 82 | 83 | (u/deftest+ 84 | "recursion works" 85 | (f/with-fakes 86 | (let [cow (f/reify-fake p/AnimalProtocol 87 | (speak :optional-fake [["you"] "moo to you!" 88 | [f/any] (fn [this _] 89 | (p/speak this "you"))]))] 90 | (is (= "moo to you!" (p/speak cow "Bob")))))) 91 | 92 | (u/deftest+ 93 | "calling a non-reified method throws an exception" 94 | (u/is-exception-thrown 95 | java.lang.AbstractMethodError 96 | js/Error 97 | #"" 98 | (f/with-fakes 99 | (let [cow (f/reify-fake p/AnimalProtocol 100 | (speak :optional-fake [f/any nil]))] 101 | (p/sleep cow))))) 102 | 103 | (u/deftest+ 104 | "config is not required for optional fake" 105 | (f/with-fakes 106 | (let [cow (f/reify-fake p/AnimalProtocol 107 | (speak :optional-fake))] 108 | (is (not= (p/speak cow "Bob") (p/speak cow "Bob")))))) 109 | 110 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; recorded-fake 111 | (u/deftest+ 112 | "simplest method can be a recorded fake" 113 | (f/with-fakes 114 | (let [cow (f/reify-fake p/AnimalProtocol 115 | (speak :recorded-fake [f/any "moo"]))] 116 | (is (= "moo" (p/speak cow))) 117 | (is (f/was-called (f/method cow p/speak) [cow]))))) 118 | 119 | (u/deftest+ 120 | "recorded fake's args matchers get correct args on call" 121 | (f/with-fakes 122 | (let [cow (f/reify-fake p/AnimalProtocol 123 | (eat :recorded-fake [["grass" "water"] "ate as usual" 124 | ["banana" nil] "ate a banana" 125 | ["" "juice"] "drank some juice"]))] 126 | (is (= "ate as usual" (p/eat cow "grass" "water"))) 127 | (is (= "drank some juice" (p/eat cow "" "juice"))) 128 | (is (= "ate a banana" (p/eat cow "banana" nil))) 129 | 130 | (is (f/method-was-called p/eat cow ["grass" "water"])) 131 | (is (f/method-was-called p/eat cow ["" "juice"])) 132 | (is (f/method-was-called p/eat cow ["banana" nil]))))) 133 | 134 | (u/deftest+ 135 | "the same method can be recorded in different fake instances" 136 | (f/with-fakes 137 | (let [cow (f/reify-fake p/AnimalProtocol 138 | (speak :recorded-fake [f/any #(str "moo, " %2)])) 139 | dog (f/reify-fake p/AnimalProtocol 140 | (speak :recorded-fake [f/any #(str "woof, " %2)]))] 141 | 142 | (is (= "moo, Bob" (p/speak cow "Bob"))) 143 | (is (= "woof, Alice" (p/speak dog "Alice"))) 144 | 145 | (is (f/method-was-called p/speak cow ["Bob"])) 146 | (is (f/method-was-called p/speak dog ["Alice"]))))) 147 | 148 | (u/deftest+ 149 | "several methods can be recorded in the fake instance" 150 | (f/with-fakes 151 | (let [cow (f/reify-fake p/AnimalProtocol 152 | (speak :recorded-fake [f/any #(str "moo, " %2)]) 153 | (sleep :recorded-fake [f/any "zzz"]))] 154 | 155 | (is (= "moo, Bob" (p/speak cow "Bob"))) 156 | (is (= "zzz" (p/sleep cow))) 157 | 158 | (is (f/method-was-called p/speak cow ["Bob"])) 159 | (is (f/method-was-called p/sleep cow []))))) 160 | 161 | (u/deftest+ 162 | "config is not required for recorded fake" 163 | (f/with-fakes 164 | (let [cow (f/reify-fake p/AnimalProtocol 165 | (speak :recorded-fake))] 166 | (is (not= (p/speak cow "Bob") (p/speak cow "Bob"))) 167 | (is (f/method-was-called p/speak cow ["Bob"]))))) 168 | 169 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; fake 170 | (u/deftest+ 171 | "simplest method can be a fake" 172 | (f/with-fakes 173 | (let [cow (f/reify-fake p/AnimalProtocol 174 | (sleep :fake [[] "zzz"]))] 175 | (is (= "zzz" (p/sleep cow)))))) 176 | 177 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; several protocols 178 | (u/deftest+ 179 | "several protocols can be reified at once with optional fakes" 180 | (f/with-fakes 181 | (let [cow (f/reify-fake p/AnimalProtocol 182 | (speak :optional-fake [f/any "moo"]) 183 | (eat :optional-fake [["grass" "water"] "om-nom"]) 184 | 185 | p/FileProtocol 186 | (save :optional-fake [f/any "saved"]))] 187 | (is (= "moo" (p/speak cow))) 188 | (is (= "saved" (p/save cow)))))) 189 | 190 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Java interfaces 191 | #?(:clj 192 | (u/deftest+ 193 | "java.lang.CharSequence/length can be reified with fake" 194 | (f/with-fakes 195 | (let [foo (f/reify-fake 196 | java.lang.CharSequence 197 | (length :fake [[] 123]))] 198 | (is (= 123 (.length foo))))))) 199 | 200 | #?(:clj 201 | (u/deftest+ 202 | "java.lang.CharSequence/charAt can be reified with optional fake" 203 | (f/with-fakes 204 | (let [foo (f/reify-fake 205 | java.lang.CharSequence 206 | (charAt :optional-fake [[100] \a]))] 207 | (is (= \a (.charAt foo 100))))))) 208 | 209 | #?(:clj 210 | (u/deftest+ 211 | "java.lang.CharSequence/subSequence can be reified with optional fake" 212 | (f/with-fakes 213 | (let [foo (f/reify-fake 214 | java.lang.CharSequence 215 | (subSequence :optional-fake [[100 200] "bar"]))] 216 | (is (= "bar" (.subSequence foo 100 200))))))) 217 | 218 | #?(:clj 219 | (u/deftest+ 220 | "java.lang.Appendable/append (overloaded method) can be reified with optional fake" 221 | (f/with-fakes 222 | (let [my-char-seq "123" 223 | expected1 (new StringBuffer) 224 | expected2 (new StringBuffer) 225 | foo (f/reify-fake 226 | java.lang.Appendable 227 | (append :optional-fake [[\a] expected1 228 | [my-char-seq] expected2]))] 229 | (is (= expected1 (.append foo \a))) 230 | (is (= expected2 (.append foo my-char-seq))))))) 231 | 232 | #?(:clj 233 | (defn int-array? 234 | [x] 235 | (= (.getName (class x)) "[I"))) 236 | 237 | #?(:clj 238 | (defn integer-array? 239 | [x] 240 | (= (.getName (class x)) "[Ljava.lang.Integer;"))) 241 | 242 | #?(:clj 243 | (defn double-array? 244 | [x] 245 | (= (.getName (class x)) "[Ljava.lang.Double;"))) 246 | 247 | #?(:clj 248 | (u/deftest+ 249 | "overloaded Java interface method with different return types can be reified with optional fake" 250 | (f/with-fakes 251 | (let [foo (f/reify-fake 252 | interop.InterfaceFixture 253 | (overloadedMethodWithDifferentReturnTypes 254 | :optional-fake 255 | [[\a] 100 256 | [\b] 200 257 | [314] 300 258 | [3.14] 400 259 | ["bar"] "500" 260 | [] "600" 261 | [true] "700" 262 | [100 200] true 263 | 264 | [(f/arg int-array?)] "int array arg" 265 | [(f/arg integer-array?)] "integer array arg" 266 | 267 | [100 200 300] (int-array [800]) 268 | [3.14 100] (into-array [(int 314) (int 100)]) 269 | 270 | [100 (f/arg integer-array?)] "integer vararg" 271 | [(f/arg double-array?)] "double vararg"]))] 272 | (is (= 100 (.overloadedMethodWithDifferentReturnTypes foo \a))) 273 | (is (= 200 (.overloadedMethodWithDifferentReturnTypes foo \b))) 274 | (is (= 300 (.overloadedMethodWithDifferentReturnTypes foo 314))) 275 | (is (= 400 (.overloadedMethodWithDifferentReturnTypes foo 3.14))) 276 | (is (= "500" (.overloadedMethodWithDifferentReturnTypes foo "bar"))) 277 | (is (= "600" (.overloadedMethodWithDifferentReturnTypes foo))) 278 | (is (= "700" (.overloadedMethodWithDifferentReturnTypes foo true))) 279 | (is (= true (.overloadedMethodWithDifferentReturnTypes foo 100 200))) 280 | 281 | (is (= "int array arg" (.overloadedMethodWithDifferentReturnTypes foo (int-array [100 200])))) 282 | (is (= "integer array arg" (.overloadedMethodWithDifferentReturnTypes foo (into-array [(int 100) (int 200)])))) 283 | 284 | (is (Arrays/equals (int-array [800]) (.overloadedMethodWithDifferentReturnTypes foo 100 200 300))) 285 | (is (Arrays/equals (into-array [(int 314) (int 100)]) (.overloadedMethodWithDifferentReturnTypes foo 3.14 100))) 286 | 287 | (is (= "integer vararg" (.overloadedMethodWithDifferentReturnTypes foo 100 (into-array [(int 200)])))) 288 | (is (= "double vararg" (.overloadedMethodWithDifferentReturnTypes foo (into-array [3.14])))))))) 289 | 290 | ; TODO: 291 | ;#?(:clj 292 | ; (u/deftest+ 293 | ; "java.lang.Appendable/append (overloaded method) can be reified with recorded fake" 294 | ; (f/with-fakes 295 | ; (let [foo (f/reify-fake 296 | ; java.lang.Appendable 297 | ; [append :recorded-fake])] 298 | ; (is (satisfies? fc/FakeReturnValue (.append foo \a))))))) 299 | 300 | #?(:clj 301 | (u/deftest+ 302 | "clojure.lang.IFn/invoke can be reified with recordable fake" 303 | (f/with-fakes 304 | (let [foo (f/reify-fake 305 | clojure.lang.IFn 306 | (invoke :recorded-fake))] 307 | (foo 123) 308 | (foo 1 2 3) 309 | 310 | (is (f/method-was-called "invoke" foo [123])) 311 | (is (f/method-was-called "invoke" foo [1 2 3])))))) 312 | 313 | #?(:cljs 314 | (u/deftest+ 315 | "IFn/-invoke can be reified with recordable fake" 316 | (f/with-fakes 317 | (let [foo (f/reify-fake 318 | IFn 319 | (-invoke :recorded-fake))] 320 | (foo 123) 321 | (foo 1 2 3) 322 | 323 | (is (f/method-was-called -invoke foo [123])) 324 | (is (f/method-was-called -invoke foo [1 2 3])))))) 325 | 326 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Object 327 | #?(:cljs 328 | (u/deftest+ 329 | "Object can be reified with a new optional-fake method" 330 | (f/with-fakes 331 | (let [foo (f/reify-fake Object 332 | (new-method1 [] :optional-fake) 333 | (new-method2 [x y] :optional-fake [[f/any f/any] #(+ %2 %3)]) 334 | (new-method3 [x y z] :optional-fake [f/any "bar"]))] 335 | (is (instance? fc/FakeReturnValue (.new-method1 foo))) 336 | 337 | (is (= 5 (.new-method2 foo 2 3))) 338 | 339 | (is (= "bar" (.new-method3 foo))) 340 | (is (= "bar" (.new-method3 foo 1))) 341 | (is (= "bar" (.new-method3 foo 1 2))) 342 | (is (= "bar" (.new-method3 foo 1 2 3))))))) 343 | 344 | #?(:cljs 345 | (u/deftest+ 346 | "Object can be reified with a new fake method" 347 | (f/with-fakes 348 | (let [foo (f/reify-fake Object 349 | (new-method1 [] :fake [[] "bla"]) 350 | (new-method2 [x y] :fake [[f/any f/any] #(+ %2 %3)]) 351 | (new-method3 [x y z] :fake [f/any "bar"]))] 352 | (is (= "bla" (.new-method1 foo))) 353 | 354 | (is (= 5 (.new-method2 foo 2 3))) 355 | 356 | (is (= "bar" (.new-method3 foo))) 357 | (is (= "bar" (.new-method3 foo 1))) 358 | (is (= "bar" (.new-method3 foo 1 2))) 359 | (is (= "bar" (.new-method3 foo 1 2 3))))))) 360 | 361 | #?(:cljs 362 | (u/deftest+ 363 | "Object can be reified with a new recorded fake method" 364 | (f/with-fakes 365 | (let [foo (f/reify-fake Object 366 | (new-method1 [x] :recorded-fake) 367 | (new-method2 [x y] :recorded-fake [[f/any f/any] #(+ %2 %3)]))] 368 | (is (instance? fc/FakeReturnValue (.new-method1 foo 777))) 369 | (is (= 5 (.new-method2 foo 2 3))) 370 | 371 | (is (f/method-was-called "new-method1" foo [777])) 372 | (is (f/method-was-called "new-method2" foo [2 3])))))) 373 | 374 | #?(:cljs 375 | (u/deftest+ 376 | "Object/toString can be faked" 377 | (f/with-fakes 378 | (let [foo (f/reify-fake Object 379 | (toString [] :recorded-fake [[] "bla"]))] 380 | (is (= "bla" (str foo))) 381 | (is (f/method-was-called "toString" foo [])))))) 382 | 383 | #?(:clj 384 | (u/deftest+ 385 | "Object/toString can be faked" 386 | (f/with-fakes 387 | (let [foo (f/reify-fake Object 388 | (toString :recorded-fake [[] "bla"]))] 389 | (is (= "bla" (str foo))) 390 | (is (f/method-was-called "toString" foo [])))))) 391 | 392 | #?(:clj 393 | (u/deftest+ 394 | "java.lang.Object is also supported" 395 | (f/with-fakes 396 | (let [foo (f/reify-fake java.lang.Object 397 | (toString :fake [[] "bla"]))] 398 | (is (= "bla" (str foo))))))) 399 | 400 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; integration 401 | (u/deftest+ 402 | "several methods can be fakes using different fake types" 403 | (f/with-fakes 404 | (let [cow (f/reify-fake p/AnimalProtocol 405 | (speak :recorded-fake [f/any #(str "moo, " %2)]) 406 | (sleep :optional-fake [f/any "zzz"]))] 407 | 408 | (is (= "moo, Bob" (p/speak cow "Bob"))) 409 | (is (= "zzz" (p/sleep cow))) 410 | (is (f/method-was-called p/speak cow ["Bob"]))))) 411 | 412 | (u/deftest+ 413 | "several methods can be fakes using different fake types (using explicit context)" 414 | (let [ctx (fc/context)] 415 | (let [cow (fc/reify-fake ctx p/AnimalProtocol 416 | (speak :recorded-fake [f/any #(str "moo, " %2)]) 417 | (sleep :optional-fake [f/any "zzz"]))] 418 | 419 | (is (= "moo, Bob" (p/speak cow "Bob"))) 420 | (is (= "zzz" (p/sleep cow))) 421 | (is (fc/method-was-called ctx p/speak cow ["Bob"]))))) 422 | 423 | (u/deftest+ 424 | "several protocols can be reified at once with different fake types" 425 | (f/with-fakes 426 | (let [cow (f/reify-fake p/AnimalProtocol 427 | (speak :recorded-fake [f/any #(str "moo, " %2)]) 428 | (sleep :optional-fake [f/any "zzz"]) 429 | 430 | p/FileProtocol 431 | (save :recorded-fake))] 432 | (p/save cow) 433 | (is (= "moo, Bob" (p/speak cow "Bob"))) 434 | (is (= "zzz" (p/sleep cow))) 435 | (is (f/method-was-called p/speak cow ["Bob"])) 436 | (is (f/method-was-called p/save cow []))))) 437 | 438 | #?(:clj 439 | (u/deftest+ 440 | "protocol and Java interface can be reified at the same time" 441 | (f/with-fakes 442 | (let [foo (f/reify-fake 443 | p/FileProtocol 444 | (save :recorded-fake) 445 | 446 | java.lang.CharSequence 447 | (charAt :recorded-fake [f/any \a]))] 448 | (p/save foo) 449 | (.charAt foo 100) 450 | 451 | (is (f/method-was-called p/save foo [])) 452 | (is (f/method-was-called "charAt" foo [100])))))) 453 | 454 | (defprotocol WebService 455 | (save [this data])) 456 | 457 | (u/deftest+ 458 | "real example: method can be faked with cyclical return values" 459 | (f/with-fakes 460 | (let [service (f/reify-fake WebService 461 | (save :fake [[:--data--] 462 | (f/cyclically [503 200])]))] 463 | (is (= 503 (save service :--data--))) 464 | (is (= 200 (save service :--data--))) 465 | (is (= 503 (save service :--data--)))))) 466 | 467 | #?(:clj 468 | ; Extracted for prettier failure reports." 469 | (defn array-arg 470 | [expected-array] 471 | (fc/arg* #(Arrays/equals % expected-array) 472 | (str "")))) 473 | 474 | #?(:clj 475 | (u/deftest+ 476 | "interface method with varargs can be reified with recorded fake" 477 | (f/with-fakes 478 | (let [foo (f/reify-fake 479 | interop.InterfaceFixture 480 | ; Without explicit return value there'll be a casting error 481 | (methodWithVarargs :recorded-fake [f/any "fake return value"]))] 482 | (.methodWithVarargs foo 100 (into-array ["bar" "baz"])) 483 | 484 | (is (f/method-was-called-once "methodWithVarargs" foo [100 (array-arg (into-array ["bar" "baz"]))])))))) -------------------------------------------------------------------------------- /docs/user-guide.md: -------------------------------------------------------------------------------- 1 | # Namespaces 2 | 3 | The public API is split into two namespaces: 4 | 5 | * `clj-fakes.core` 6 | * `clj-fakes.context` 7 | 8 | These namespaces contain almost the same set of members. The difference 9 | is that `core` uses an implicit context and the `context` namespace 10 | functions require an explicit context argument. 11 | 12 | For your convenience functions which don't rely on a context can also be sometimes found in both namespaces (e.g. `f/any` is the same as `fc/any`). 13 | 14 | The private/internal API uses a `-` prefix and should not be used (e.g. `-this-is-some-private-thing`). 15 | 16 | # Context 17 | 18 | Context is an object which stores all the information about 19 | created fakes (recorded calls, positions in code, etc.). 20 | All fakes have to be created inside some context. 21 | 22 | To create a new context use `clj-fakes.context/context`: 23 | 24 | ```clj 25 | ; explicit context 26 | (let [ctx (fc/context) 27 | foo (fc/recorded-fake ctx)] 28 | ; ... 29 | ) 30 | ``` 31 | 32 | Alternatively a new context can be created with 33 | `clj-fakes.core/with-fakes` macro: 34 | 35 | ```clj 36 | ; implicit context 37 | (f/with-fakes 38 | ; note that now fake is created using a macro from core ns 39 | (let [foo (f/recorded-fake)] 40 | ; ... 41 | )) 42 | ``` 43 | 44 | This approach is preferable since it requires less typing, automatically 45 | unpatches all [patched vars](#monkey-patching) and executes [self-tests](#self-tests). 46 | 47 | Internally `with-fakes` relies on a public dynamic variable `*context*` which can be 48 | used in your own helper functions. 49 | 50 | # Function Fakes 51 | 52 | Fake is a function which returns canned values on matched arguments and can optionally record its calls. It 53 | can be used to define and assert a behavior of a functional dependency of an SUT (system under test). 54 | 55 | ## Fake 56 | 57 | A regular fake function can be created using a macro: 58 | 59 | `(f/fake config)` 60 | 61 | `(fc/fake ctx config)` 62 | 63 | [Config](#fake-configuration) is a vector which defines which values to return for different arguments: 64 | 65 | ```clj 66 | (let [foo (f/fake [[1 2] "foo" 67 | [3 4 5] "bar"])] 68 | (foo 1 2) ; => "foo" 69 | (foo 3 4 5)) ; => "bar" 70 | ``` 71 | 72 | If passed arguments cannot be [matched](#argument-matching) using specified 73 | config then the exception will be raised: 74 | 75 | ```clj 76 | (foo 100 200) ; => raises "Unexpected args are passed into fake: (100 200) ..." 77 | ``` 78 | 79 | A fake is assumed to be called at least once inside the context; otherwise, [self-test](#self-tests) exception 80 | will be raised. In such case user should either modify a test, an SUT 81 | or consider using an [optional fake](#optional-fake): 82 | 83 | ```clj 84 | (f/with-fakes 85 | (f/fake [[] nil])) ; => raises "Self-test: no call detected for: non-optional fake ..." 86 | ``` 87 | 88 | If your test scenario focuses on testing a behavior (e.g. "assert that foo was called by an SUT") then do not rely on self-tests, 89 | instead use [recorded fakes](#recorded-fake) with explicit [assertions](#assertions). 90 | Self-tests are more about checking usefulness of provided preconditions than 91 | about testing expected behavior. 92 | 93 | ## Optional Fake 94 | 95 | `(f/optional-fake [config])` 96 | 97 | `(fc/optional-fake ctx [config])` 98 | 99 | It works the same as a regular fake but is not expected to be always called in the context: 100 | 101 | ```clj 102 | (f/with-fakes 103 | (f/optional-fake [[1 2] 3])) ; => ok, self-test will pass 104 | ``` 105 | 106 | Such fakes should be used to express the intent of the test writer, 107 | for example, when you have to provide a dependency to an SUT, 108 | but this dependency is not really related to the test case: 109 | 110 | ```clj 111 | (defn process-payments 112 | "Processor requires a logger." 113 | [data logger] 114 | {:pre [(fn? logger)]} 115 | ; ... 116 | ) 117 | 118 | (deftest good-payments-are-processed-without-error 119 | (f/with-fakes 120 | (let [; ... 121 | ; we are not interested in how logger is going to be used, just stub it and forget 122 | fake-logger (f/optional-fake)] 123 | (is (= :success (process-payments good-payments fake-logger)))))) 124 | ``` 125 | 126 | As you may have noticed, `config` argument can be omitted. 127 | In such case fake will be created with [`default-fake-config`](#fake-configuration) 128 | which allows any arguments to be passed on invocation. 129 | 130 | ## Recorded Fake 131 | 132 | Invocations of this fake are recorded so that they can later be asserted: 133 | 134 | `(f/recorded-fake [config])` 135 | 136 | `(fc/recorded-fake ctx [config])` 137 | 138 | Use `calls` function in order to get all recorded invocations for the specified 139 | recorded fake. 140 | It can also return all the recorded calls in the context if fake is not specified: 141 | 142 | ```clj 143 | (let [foo (f/recorded-fake [[(f/arg integer?) (f/arg integer?)] #(+ %1 %2)]) 144 | bar (f/recorded-fake [[(f/arg integer?) (f/arg integer?)] #(* %1 %2)])] 145 | (foo 1 2) 146 | (bar 5 6) 147 | (foo 7 8) 148 | 149 | (f/calls foo) 150 | ; => [{:args [1 2] :return-value 3} 151 | ; {:args [7 8] :return-value 15}] 152 | 153 | (f/calls) 154 | ; => [[foo {:args [1 2] :return-value 3}] 155 | ; [bar {:args [5 6] :return-value 30}] 156 | ; [foo {:args [7 8] :return-value 15}]] 157 | ) 158 | ``` 159 | 160 | Recorded fake must be checked using one of the [assertions](#assertions) provided by the framework or 161 | be marked as checked explicitly using `mark-checked` function; 162 | otherwise, [self-test](#unchecked-fakes) will raise an exception: 163 | 164 | ```clj 165 | (f/with-fakes 166 | (f/recorded-fake)) ; => raises "Self-test: no check performed on: recorded fake ..." 167 | ``` 168 | 169 | ```clj 170 | (f/with-fakes 171 | (let [foo (f/recorded-fake)] 172 | (foo) 173 | (is (f/was-called foo [])))) ; => ok, self-test will pass 174 | ``` 175 | 176 | ```clj 177 | (f/with-fakes 178 | (f/mark-checked (f/recorded-fake))) ; => ok, self-test will pass 179 | ``` 180 | 181 | ## Custom Macros 182 | 183 | In your own reusable macros you should use `fake*/recorded-fake*` 184 | instead of `fake/recorded-fake`: 185 | 186 | `(f/fake* form config)` 187 | 188 | `(fc/fake* ctx form config)` 189 | 190 | `(f/recorded-fake* form [config])` 191 | 192 | `(fc/recorded-fake* ctx form [config])` 193 | 194 | In other words, your macro must explicitly provide `&form` to framework macros; 195 | otherwise, due to implementation details, framework will not be 196 | able to correctly determine fake function line numbers which is crucial for debugging. 197 | 198 | The framework will warn you if you accidentally use the version without asterisk 199 | in your macro. 200 | 201 | # Fake Configuration 202 | 203 | Fake config should contain pairs of [args matcher](#argument-matchinga) and return value: 204 | 205 | ```clj 206 | [args-matcher1 fn-or-value1 207 | args-matcher2 fn-or-value2 ...] 208 | ``` 209 | 210 | On fake invocation matchers will be tested from top to bottom and 211 | on the first match the specified value will be returned. 212 | If return value is a function than it will be called with passed arguments to generate the return value at runtime: 213 | 214 | ```clj 215 | (let [foo (f/fake [[1 2] 100 216 | [3 4] #(+ %1 %2) 217 | [5 6] (fn [_ _] (throw (ex-info "wow" {})))])] 218 | (foo 1 2) ; => 100 219 | (foo 3 4) ; => 7 220 | (foo 5 6)) ; => raises "wow" exception 221 | ``` 222 | 223 | There's one built-in config in the framework: 224 | 225 | `fc/default-fake-config` 226 | 227 | It accepts any number of arguments and returns a new unique 228 | instance of type `FakeReturnValue` on each call. 229 | It is used by `optional-fake` and `recorded-fake` functions by default (i.e. when user 230 | doesn't specify the config explicitly). 231 | 232 | ## Helpers 233 | 234 | `(f/cyclically coll)` 235 | 236 | `(fc/cyclically coll)` 237 | 238 | This function can be used to implement iterator-style stubbing 239 | when a fake returns a different value on each call: 240 | 241 | ```clj 242 | (let [get-weekday (f/fake [["My event"] (f/cyclically [:monday :tuesday :wednesday])])] 243 | (is (= :monday (get-weekday "My event"))) 244 | (is (= :tuesday (get-weekday "My event"))) 245 | (is (= :wednesday (get-weekday "My event"))) 246 | (is (= :monday (get-weekday "My event")))) 247 | ``` 248 | 249 | # Argument Matching 250 | 251 | Every arguments matcher must implement an `fc/ArgsMatcher` protocol: 252 | 253 | ```clj 254 | (defprotocol ArgsMatcher 255 | (args-match? [this args] "Should return true or false.") 256 | (args-matcher->str [this] "Should return a string for debug messages.")) 257 | ``` 258 | 259 | In most cases you won't need to create instances of this protocol manually 260 | because framework provides vector matchers which are useful in most cases. 261 | 262 | ## Vector Matcher 263 | 264 | Vector matchers were already used all other this guide. 265 | Each vector element can be an expected value or an `fc/ImplicitArgMatcher` instance: 266 | 267 | ```clj 268 | [implicit-arg-matcher-or-exact-value1 implicit-arg-matcher-or-exact-value2 ...] 269 | ``` 270 | 271 | ```clj 272 | (defprotocol ImplicitArgMatcher 273 | (arg-matches-implicitly? [this arg] "Should return true or false.") 274 | (arg-matcher->str [this] "Should return a string for debug messages.")) 275 | ``` 276 | 277 | It is not recommended to extend existing types with `ImplicitArgMatcher` protocol; 278 | instead, to make code more explicit and future-proof, 279 | you should use an `arg` "adapter" macro and pass it `ArgMatcher` instances: 280 | 281 | ```clj 282 | (let [foo (f/fake [[] "no args" 283 | [[]] "empty vector" 284 | [1 2] "1 2" 285 | [(f/arg integer?) (f/arg integer?)] "two integers" 286 | [(f/arg string?)] "string"])] 287 | (foo) ; => "no args" 288 | (foo []) ; => "empty vector" 289 | (foo 1 2) ; => "1 2" 290 | (foo 1 2 3) ; => exception: "Unexpected args are passed into fake: (1 2 3) ..." 291 | (foo 100 200) ; => "two integers" 292 | (foo "hey")) ; => "string" 293 | ``` 294 | 295 | As you can see, the framework already supports *functional argument matchers* 296 | which are implemented by extending function type like this: 297 | 298 | ```clj 299 | (extend-type #?(:clj clojure.lang.Fn 300 | :cljs function) 301 | ArgMatcher 302 | (arg-matches? [this arg] 303 | (this arg))) 304 | ``` 305 | 306 | You are encouraged to define your own argument matchers in a similar way. 307 | 308 | The framework also supports *regex matchers* (using 309 | [`re-find`](https://clojuredocs.org/clojure.core/re-find) under the hood), for example: `(f/arg #"abc.*")`. 310 | 311 | ## any 312 | 313 | `(f/any _)` 314 | 315 | `(fc/any _)` 316 | 317 | This special matcher always returns `true` for any input arguments. 318 | It can be used to match single and multiple arguments: 319 | 320 | ```clj 321 | (let [foo (f/fake [[1 2] "1 2" 322 | [f/any f/any f/any] "three args" 323 | f/any "something else"])] 324 | (foo) ; => "something else" 325 | (foo 1) ; => "something else" 326 | (foo 1 2) ; => "1 2" 327 | (foo 1 2 3) ; => "three args" 328 | (foo 1 2 3 4)) ; => "something else" 329 | ``` 330 | 331 | # Protocol Fakes 332 | 333 | Framework defines two new macros for reifying protocols 334 | using function fakes described earlier. So, for example, 335 | you can record and assert method calls on reified instances. 336 | 337 | The "strict" `reify-fake` macro is very similar to `reify`; in particular, 338 | created instance will raise an exception 339 | on calling protocol method which is not defined. 340 | 341 | On the other hand, `reify-nice-fake` is able to automatically 342 | generate [optional-fake](#optional-fake) implementations for methods which are 343 | not explicitly defined by user. 344 | 345 | Which macro to use solely depends on your testing style. I'd 346 | recommend to use nice fakes whenever possible in order to make 347 | tests more compact and break less often on code changes. 348 | 349 | There are some subtleties, so here's a table to give you an overview of 350 | which features are currently supported: 351 | 352 | Feature | `reify-fake` | `reify-nice-fake` 353 | - | - | - 354 | Fake protocol method (explicitly) | Yes | Yes 355 | Fake protocol method (auto) | - | Yes 356 | Fake Java interface method (explicitly) | Yes | Yes 357 | Fake Java interface method (auto) | - | No 358 | Fake Object method (explicitly) | Yes | Only in Clojure 359 | Fake Object method (auto) | - | No 360 | Object can be reified with any new methods | Only in ClojureScript | Only in ClojureScript 361 | Support overloaded methods | Yes | Yes 362 | 363 | ## Syntax 364 | 365 | The syntax is very similar to the built-in `reify` macro: 366 | 367 | `(f/reify-fake specs*)` 368 | 369 | `(fc/reify-fake ctx specs*)` 370 | 371 | `(f/reify-nice-fake specs*)` 372 | 373 | `(fc/reify-nice-fake ctx specs*)` 374 | 375 | Each spec consists of the protocol or interface name followed by zero 376 | or more method fakes: 377 | 378 | ```clj 379 | protocol-or-interface-or-Object 380 | (method-name [arglist] fake-type [config])* 381 | ``` 382 | 383 | Available fake types: 384 | 385 | * `:fake` (see [Fake](#fake)) 386 | * `:optional-fake` (see [Optional Fake](#optional-fake)) 387 | * `:recorded-fake` (see [Recorded Fake](#recorded-fake)) 388 | 389 | As with function fakes, config can be omitted for `:optional-fake` and `:recorded-fake`: 390 | 391 | ```clj 392 | (defprotocol AnimalProtocol 393 | (speak [this] [this name] [this name1 name2]) 394 | (eat [this food drink]) 395 | (sleep [this])) 396 | 397 | (defprotocol FileProtocol 398 | (save [this]) 399 | (scan [this])) 400 | 401 | ; ... 402 | 403 | (f/reify-fake 404 | p/AnimalProtocol 405 | (sleep :fake [f/any "zzz"]) 406 | (speak :recorded-fake) 407 | 408 | p/FileProtocol 409 | (save :optional-fake) 410 | 411 | java.lang.CharSequence 412 | (charAt :recorded-fake [f/any \a])) 413 | ``` 414 | 415 | Although protocol methods always have a first `this` argument, 416 | method configs must not try to match this argument. 417 | However, the return value function will receive all the arguments on invocation, 418 | including `this`: 419 | 420 | ```clj 421 | (let [monkey (f/reify-fake p/AnimalProtocol 422 | ; config only matches |food| and |drink| arguments 423 | ; but return value function will get all 3 arguments on call 424 | (eat :fake [[f/any f/any] #(str "ate " %2 " and drank " %3)]))] 425 | (println (p/eat monkey "banana" "water"))) ; => ate banana and drank water 426 | ``` 427 | 428 | In ClojureScript it's possible to implement any methods under `Object` protocol. 429 | The framework supports this scenario but requires an arglist explicitly specified after the method name 430 | (`this` arg should be omitted): 431 | 432 | ```clj 433 | (let [calculator (f/reify-fake Object 434 | (sum [x y] :fake [[f/any f/any] #(+ %2 %3)]) 435 | (toString [] :optional-fake [[] "my calculator"]))] 436 | (is (= 5 (.sum calculator 2 3))) 437 | (is (= "my calculator" (str calculator)))) 438 | ``` 439 | 440 | ## Calls & Assertions 441 | 442 | In order to get and assert recorded method calls there's a 443 | helper function: 444 | 445 | `(f/method obj f)` 446 | 447 | `(fc/method ctx obj f)` 448 | 449 | It can be used in combination with existing `calls` and `was-called-*` functions like this: 450 | 451 | ```clj 452 | (f/with-fakes 453 | (let [cow (f/reify-fake p/AnimalProtocol 454 | (speak :recorded-fake [f/any "moo"]))] 455 | (p/speak cow) 456 | (println (f/calls (f/method cow p/speak))) ; => [{:args ..., :return-value moo}] 457 | (is (f/was-called-once (f/method cow p/speak) [cow])))) 458 | ``` 459 | 460 | Notice how object name `cow` is duplicated at the last line. In order to get 461 | rid of such duplications there are additional `method-*` assertions defined. 462 | So the last expression can be rewritten like this: 463 | 464 | ```clj 465 | (is (f/method-was-called-once p/speak cow [])) 466 | ``` 467 | 468 | For the list of all available assertion functions see [Assertions](#assertions). 469 | 470 | There's a quirk when Java interface or ClojureScript `Object` method is faked: you will need to use its 471 | string representation in `method`/`method-*`: 472 | 473 | ```clj 474 | (let [foo (f/reify-fake clojure.lang.IFn 475 | (invoke :recorded-fake))] 476 | (foo 1 2 3) 477 | (is (f/method-was-called "invoke" foo [1 2 3]))) 478 | ``` 479 | 480 | ## Custom Macros 481 | 482 | In your own reusable macros you should use `reify-fake*/reify-nice-fake*` 483 | instead of `reify-fake/reify-nice-fake`: 484 | 485 | `(f/reify-fake* form env specs*)` 486 | 487 | `(fc/reify-fake* ctx form env specs*)` 488 | 489 | `(f/reify-nice-fake* form env specs*)` 490 | 491 | `(fc/reify-nice-fake* ctx form env specs*)` 492 | 493 | In other words, your macro must explicitly provide `&form` and `&env` to framework macros; 494 | otherwise, due to implementation details, framework will not be 495 | able to correctly determine fake method line numbers which is crucial for debugging. 496 | 497 | For instance: 498 | 499 | ```clj 500 | (defmacro my-reify-fake 501 | [& specs] 502 | `(f/reify-fake* ~&form ~&env ~@specs)) 503 | ``` 504 | 505 | The framework will warn you if you accidentally use the version without asterisk 506 | in your macro. 507 | 508 | # Assertions 509 | 510 | Framework provides several assertion functions for [recorded fakes](#recorded-fake). 511 | Each function either returns `true` or raises an exception with additional details: 512 | 513 | `(f/was-called-once f args-matcher)` 514 | - checks that function was called strictly once and that the call was with the specified args. 515 | 516 | `(f/was-called f args-matcher)` 517 | - checks that function was called at least once with the specified args. 518 | 519 | `(f/was-matched-once f args-matcher)` 520 | - checks that function was called at least once and only a single call satisfies the provided args matcher. 521 | 522 | `(f/was-not-called f)` 523 | - checks that function was never called. 524 | 525 | `(f/were-called-in-order f1 args-matcher1 f2 args-matcher2 ...)` 526 | - checks that functions were called in specified order (but it doesn't guarantee there were no other calls). 527 | 528 | The set of similar functions is defined for [protocol methods](#calls-assertions): 529 | 530 | `(f/method-was-called-once f obj args-matcher)` 531 | 532 | `(f/method-was-called f obj args-matcher)` 533 | 534 | `(f/method-was-matched-once f obj args-matcher)` 535 | 536 | `(f/method-was-not-called f obj)` 537 | 538 | `(f/methods-were-called-in-order f1 obj1 args-matcher1 f2 obj2 args-matcher2 ...)` 539 | 540 | Of course, all these functions can be called with an explicit context, e.g.: 541 | 542 | `(fc/was-called-once ctx f args-matcher)` 543 | 544 | # Self-tests 545 | 546 | Framework can perform "self-tests" in order to inform a user 547 | early on that some fakes (including protocol method fakes) are potentially used inappropriately. 548 | 549 | If you use [`with-fakes`](#context) macro then self-tests will be run automatically on exiting the block. 550 | Otherwise, when [explicit context](#context) is used, you have to invoke self-tests manually 551 | using next function: 552 | 553 | `(fc/self-test ctx)` 554 | 555 | Each test can also be run manually using dedicated functions. 556 | Currently two types of self-tests are supported to identify: 557 | 558 | * unused fakes 559 | * unchecked fakes 560 | 561 | ## Unused Fakes 562 | 563 | `(fc/self-test-unused-fakes ctx)` 564 | 565 | This function raises an exception when some [fake](#fake) was never called after its creation. 566 | 567 | For example, this self-test comes in handy when SUT stops using a dependency which 568 | was faked in several test scenarios. In such case the framework will guide you in cleaning 569 | your test suite from the unused stubs. 570 | 571 | ## Unchecked Fakes 572 | 573 | `(fc/self-test-unchecked-fakes ctx)` 574 | 575 | This self-test raises an exception if some `recorded-fake` 576 | was never [marked checked](#recorded-fake), i.e. you forgot to assert its calls. 577 | 578 | # Monkey Patching 579 | 580 | You can temporarily change a variable value by using `patch!` macro: 581 | 582 | `(f/patch! var-expr val)` 583 | 584 | `(fc/patch! ctx var-expr val)` 585 | 586 | After patching original value can still be obtained using a function: 587 | 588 | `(f/original-val a-var)` 589 | 590 | `(fc/original-val ctx a-var)` 591 | 592 | Also don't forget to unpatch the variable to recover its original value: 593 | 594 | `(f/unpatch! var-expr)` 595 | 596 | `(fc/unpatch! ctx var-expr)` 597 | 598 | Or unpatch all the variables inside the context at once: 599 | 600 | `(f/unpatch-all!)` 601 | 602 | `(fc/unpatch-all! ctx)` 603 | 604 | If you use `with-fakes` then all variables will be unpatched 605 | automatically on exiting the block, for instance: 606 | 607 | ```clj 608 | (f/with-fakes 609 | (f/patch! #'funcs/sum (f/fake [[1 2] "foo" 610 | [3 4] "bar"])) 611 | (is (= "foo" (funcs/sum 1 2))) 612 | (is (= "bar" (funcs/sum 3 4)))) 613 | 614 | ; patching is reverted on exiting with-fakes block 615 | (is (= 3 (funcs/sum 1 2))) 616 | ``` 617 | 618 | Another example is combining `patch` and `recorded-fake` in order 619 | to create a *function spy* which works exactly the same as the original function 620 | and also records its calls: 621 | 622 | ```clj 623 | (f/patch! #'funcs/sum (f/recorded-fake [f/any funcs/sum])) 624 | ``` 625 | 626 | Monkey patching is not thread-safe because it changes variable 627 | in all threads 628 | (underlying implementation uses 629 | [`alter-var-root`](https://clojuredocs.org/clojure.core/alter-var-root)/[`set!`](https://github.com/cljsinfo/cljs-api-docs/blob/catalog/refs/special/setBANG.md)). 630 | 631 | Starting from Clojure 1.8, if [direct linking](http://clojure.org/reference/compilation#directlinking) is enabled: 632 | 633 | * you have to add `^:redef` metadata key to functions which you patch; 634 | * you can't patch core functions (e.g. `println`). -------------------------------------------------------------------------------- /src/clj_fakes/context.cljc: -------------------------------------------------------------------------------- 1 | (ns clj-fakes.context 2 | "API for working in explicit context." 3 | (:require [clojure.string :as string] 4 | [clojure.pprint :as pprint] 5 | #?@(:clj [[clj-fakes.macro :as m] 6 | [clj-fakes.reflection :as r]])) 7 | 8 | ; declare macros for export 9 | #?(:cljs (:require-macros 10 | [clj-fakes.context :refer 11 | [arg 12 | fake* 13 | fake 14 | recorded-fake* 15 | recorded-fake 16 | reify-fake* 17 | -reify-fake-debug* 18 | reify-nice-fake* 19 | reify-fake 20 | reify-nice-fake 21 | patch!]]))) 22 | 23 | (defn context 24 | "Creates a new context atom. 25 | 26 | Do not alter the atom manually, context fields should be considered a private API. 27 | 28 | Also see: [[clj-fakes.core/with-fakes]]." 29 | [] 30 | (atom {; Contains pairs: [fake call] 31 | :calls [] 32 | 33 | ; Set of recorded fakes 34 | :recorded-fakes #{} 35 | 36 | ; f -> position 37 | :positions {} 38 | 39 | ; Vector of fakes 40 | :unused-fakes [] 41 | 42 | ; Vector of fakes 43 | :unchecked-fakes [] 44 | 45 | ; fake -> description; will be used for debugging 46 | :fake-descs {} 47 | 48 | ; object -> id 49 | :object-ids {} 50 | 51 | ; var -> original-var-val 52 | :original-vals {} 53 | 54 | ; var -> function that should be called to restore original value 55 | :unpatches {}})) 56 | 57 | ;;;;;;;;;;;;;;;;;;;;;;;; Args matching 58 | (defprotocol ArgsMatcher 59 | "Protocol for multiple args matching." 60 | (args-match? [this args] "Should return true or false.") 61 | (args-matcher->str [this] "Should return a string for debug messages.")) 62 | 63 | (defprotocol ImplicitArgMatcher 64 | "Most likely you shouldn't use this protocol. 65 | Consider creating explicit custom matchers by implementing [[ArgMatcher]] protocol and using [[arg]] macro." 66 | (arg-matches-implicitly? [this arg] "Should return true or false.") 67 | (arg-matcher->str [this] "Should return a string for debug messages.")) 68 | 69 | (defprotocol ArgMatcher 70 | "Protocol for explicit arg matchers. 71 | 72 | Also see: [[arg]]." 73 | (arg-matches? [this arg] "Should return true or false.")) 74 | 75 | (def ^{:doc "Matcher which matches any value. Implements both [[ArgsMatcher]] and [[ImplicitArgMatcher]]."} 76 | any 77 | (reify 78 | ArgsMatcher 79 | (args-match? [_ _args] true) 80 | (args-matcher->str [_] "") 81 | 82 | ImplicitArgMatcher 83 | (arg-matches-implicitly? [_ _arg] true) 84 | (arg-matcher->str [_] ""))) 85 | 86 | (defn ^:no-doc -arg-matches? 87 | [matcher arg] 88 | (if (satisfies? ImplicitArgMatcher matcher) 89 | (arg-matches-implicitly? matcher arg) 90 | (= arg matcher))) 91 | 92 | (extend-type #?(:clj clojure.lang.PersistentVector 93 | :cljs cljs.core.PersistentVector) 94 | ArgsMatcher 95 | (args-match? 96 | [this args] 97 | (or (and (empty? this) (empty? args)) 98 | (and (= (count this) (count args)) 99 | (every? true? (map -arg-matches? this args))))) 100 | 101 | (args-matcher->str 102 | [this] 103 | (str "[" 104 | (string/join " " (map #(if (satisfies? ImplicitArgMatcher %) 105 | (arg-matcher->str %) 106 | (str %)) 107 | this)) 108 | "]"))) 109 | 110 | (defn arg* 111 | "The same as [[arg]] macro, but string for printing the matcher must be specified explicitly." 112 | [matcher matcher-str] 113 | {:pre [(satisfies? ArgMatcher matcher) (string? matcher-str)]} 114 | (reify ImplicitArgMatcher 115 | (arg-matches-implicitly? [_ arg] (arg-matches? matcher arg)) 116 | (arg-matcher->str [_] matcher-str))) 117 | 118 | #?(:clj 119 | (defmacro arg 120 | "Creates an [[ImplicitArgMatcher]] from the specified [[ArgMatcher]] to be used in vector args matcher." 121 | [matcher] 122 | `(arg* ~matcher ~(str "<" matcher ">")))) 123 | 124 | ; functional arg matcher 125 | (extend-type #?(:clj clojure.lang.Fn 126 | :cljs function) 127 | ArgMatcher 128 | (arg-matches? [this arg] (this arg))) 129 | 130 | ; regex arg matcher 131 | (extend-type #?(:clj java.util.regex.Pattern 132 | :cljs js/RegExp) 133 | ArgMatcher 134 | (arg-matches? [this arg] (not (nil? (re-find this arg))))) 135 | 136 | (defn ^:no-doc -with-any-this-arg 137 | "Args matcher decorator which allows any 'this' arg (it is a first arg). 138 | The rest of the args will be checked by specified matcher. 139 | Returns a new matcher." 140 | [rest-args-matcher] 141 | {:pre [(satisfies? ArgsMatcher rest-args-matcher)]} 142 | (reify ArgsMatcher 143 | (args-match? 144 | [_ args] 145 | (args-match? rest-args-matcher (rest args))) 146 | 147 | (args-matcher->str 148 | [_] 149 | (str " " (args-matcher->str rest-args-matcher))))) 150 | 151 | ;;;;;;;;;;;;;;;;;;;;;;;; Utils 152 | (defn ^:no-doc -find-first 153 | "Returns nil if element wasn't found." 154 | [pred coll] 155 | (first (filter pred coll))) 156 | 157 | (defn ^:no-doc -find-and-take-after 158 | "Returns the first found element and the seq of elements after it. 159 | Returns [nil _] if element was not found." 160 | [pred coll] 161 | (let [s (drop-while (complement pred) coll)] 162 | [(first s) (rest s)])) 163 | 164 | (defn ^:no-doc -take-nth-from-group 165 | "Partitions collection by group-len and returns a lazy seq of every nth element from each partition." 166 | [n group-len coll] 167 | (take-nth group-len (drop (dec n) coll))) 168 | 169 | ;;;;;;;;;;;;;;;;;;;;;;;; Fakes - core 170 | (defn ^:no-doc -config->fn 171 | "Constructs a function from a config vector: 172 | [args-matcher1 fn-or-value1 173 | args-matcher2 fn-or-value2 ...]" 174 | [config] 175 | {:pre [(vector? config) 176 | (seq config) 177 | (even? (count config)) 178 | (->> (apply hash-map config) (keys) (every? #(satisfies? ArgsMatcher %)))] 179 | :post [(fn? %)]} 180 | (fn [& args] 181 | (let [config-pairs (partition 2 config) 182 | matched-rule (-find-first #(args-match? (first %1) args) 183 | config-pairs) 184 | return-value (second matched-rule)] 185 | (if (nil? matched-rule) 186 | (throw (ex-info (str "Unexpected args are passed into fake: " args 187 | ".\nSupported args matchers:\n" 188 | (string/join "\n" (map args-matcher->str (-take-nth-from-group 1 2 config)))) 189 | {})) 190 | (if (fn? return-value) 191 | (apply return-value args) 192 | return-value))))) 193 | 194 | (defn ^:no-doc -optional-fake 195 | "Extracted for code readability." 196 | [ctx f] 197 | {:pre [ctx (fn? f)]} 198 | f) 199 | 200 | (defn ^:no-doc -mark-used 201 | "Self-test will not warn about specified fake after using this function." 202 | [ctx f] 203 | {:pre [ctx (fn? f)]} 204 | (swap! ctx update-in [:unused-fakes] #(remove #{f} %))) 205 | 206 | (defn ^:no-doc -set-position 207 | "Saves a position for the specified fake. It can be later used for debugging." 208 | [ctx f position] 209 | {:pre [ctx f]} 210 | (swap! ctx assoc-in [:positions f] position)) 211 | 212 | (defn ^:no-doc -position 213 | "Returns a position of the specified fake." 214 | [ctx f] 215 | {:pre [ctx f]} 216 | (get (:positions @ctx) f)) 217 | 218 | (defn ^:no-doc -required 219 | [ctx position f] 220 | {:pre [ctx position (fn? f)]} 221 | (letfn [(wrapper [& args] 222 | (-mark-used ctx wrapper) 223 | (apply f args))] 224 | (swap! ctx update-in [:unused-fakes] conj wrapper) 225 | (-set-position ctx wrapper position) 226 | wrapper)) 227 | 228 | (defn ^:no-doc -fake 229 | [ctx position f] 230 | (-required ctx position (-optional-fake ctx f))) 231 | 232 | (defn ^:no-doc -record-call 233 | [ctx f call] 234 | (swap! ctx update-in [:calls] conj [f call])) 235 | 236 | (defn ^:no-doc -recorded-as 237 | "Decorates the specified function f in order to record its calls. 238 | Calls will be recorded by the specified key k. 239 | If k is nil - calls will be recorded using the decorated function as a key (in this case use -recorded). 240 | Returns decorated function." 241 | [ctx position k f] 242 | {:pre [ctx position (fn? f)] 243 | :post [(fn? %)]} 244 | (letfn [(wrapper [& args] 245 | (let [return-value (apply f args)] 246 | (-record-call ctx (or k wrapper) {:args args 247 | :return-value return-value}) 248 | return-value))] 249 | (swap! ctx update-in [:recorded-fakes] conj (or k wrapper)) 250 | (swap! ctx update-in [:unchecked-fakes] conj (or k wrapper)) 251 | (-set-position ctx (or k wrapper) position) 252 | wrapper)) 253 | 254 | (defn ^:no-doc -recorded 255 | "Decorates the specified function in order to record its calls. 256 | Calls will be recorded by the returned decorated function." 257 | [ctx position f] 258 | (-recorded-as ctx position nil f)) 259 | 260 | (defn ^:no-doc -recorded? 261 | [ctx f] 262 | (contains? (:recorded-fakes @ctx) f)) 263 | 264 | (defn mark-checked 265 | "After using this function [[self-test-unchecked-fakes]] will not warn about the specified recorded fake." 266 | [ctx f] 267 | {:pre [ctx (-recorded? ctx f)]} 268 | (swap! ctx update-in [:unchecked-fakes] #(remove #{f} %))) 269 | 270 | (defn ^:no-doc -set-desc 271 | "Saves a description for the specified fake. It can be later used for debugging." 272 | [ctx f desc] 273 | {:pre [ctx f]} 274 | (swap! ctx assoc-in [:fake-descs f] desc)) 275 | 276 | ; Type with meaningful name for creating unique return values 277 | (deftype FakeReturnValue [] 278 | Object 279 | ; make debug output less noisy 280 | (toString [_] "")) 281 | 282 | (alter-meta! #'->FakeReturnValue assoc :no-doc true) 283 | 284 | ;;;;;;;;;;;;;;;;;;;;;;;; Fakes - API 285 | (def default-fake-config 286 | "With this config fake will return a new `FakeReturnValue` type instance for any combination of args." 287 | [any (fn [& _] (FakeReturnValue.))]) 288 | 289 | (defn optional-fake 290 | "Creates an optional fake function which will not be checked by [[self-test-unused-fakes]], 291 | i.e. created fake is allowed to be never called. 292 | 293 | Config is a vector of pairs: `[args-matcher1 fn-or-value1 args-matcher2 fn-or-value2 ...]` 294 | 295 | If `config` is not specified then fake will be created with [[default-fake-config]]." 296 | ([ctx] (optional-fake ctx default-fake-config)) 297 | ([ctx config] {:pre [ctx]} (-optional-fake ctx (-config->fn config)))) 298 | 299 | #?(:clj 300 | (defn ^:no-doc -emit-fake-fn-call-with-position 301 | "Emits code with call to specified fake-fn with correct position arg. 302 | Line and column are retrieved from macro's &form var which must be passed explicitly. 303 | Filepath is retrieved from a dummy var metadata. 304 | Works in Clojure and ClojureScript, but can produce slightly different filepaths." 305 | [fake-fn ctx form & args] 306 | (assert (meta form) 307 | (str "Meta must be defined in order to detect fake position. " 308 | "Passed form: " form 309 | "\nHINT: Meta can be undefined when fake macro is invoked from another macro, " 310 | "in such case use a variation of invoked macro with a 'form' param. " 311 | "E.g. call (fake* &form...) instead of (fake ...).")) 312 | (let [{:keys [line column]} (meta form) 313 | filepath-catcher-sym (gensym "position-catcher")] 314 | `(do 315 | (def ~filepath-catcher-sym nil) 316 | (let [position# {:file (:file (meta #'~filepath-catcher-sym)) 317 | :line ~line 318 | :column ~column}] 319 | (~fake-fn ~ctx position# ~@args)))))) 320 | 321 | (defn ^:no-doc -fake** 322 | "This function is the same as fake macro, but position must be passed explicitly." 323 | [ctx position config] 324 | (-fake ctx position (-config->fn config))) 325 | 326 | #?(:clj 327 | (defmacro fake* 328 | "The same as [[fake]] macro but for reuse in other macros. 329 | 330 | `form` must be passed from the parent macro in order to correctly detect position." 331 | [ctx form config] 332 | (-emit-fake-fn-call-with-position `-fake** ctx form config))) 333 | 334 | #?(:clj 335 | (defmacro fake 336 | "Creates a fake function. The created function must be called; otherwise, [[self-test-unused-fakes]] will fail. 337 | 338 | Config is a vector of pairs: `[args-matcher1 fn-or-value1 args-matcher2 fn-or-value2 ...]` 339 | 340 | Use [[fake*]] in case this macro is called from another macro." 341 | [ctx config] 342 | `(fake* ~ctx ~&form ~config))) 343 | 344 | (defn ^:no-doc -recorded-fake** 345 | "This function is the same as recorded-fake macro, but position must be passed explicitly." 346 | ([ctx position] (-recorded-fake** ctx position default-fake-config)) 347 | ([ctx position config] (-recorded ctx position (-config->fn config)))) 348 | 349 | #?(:clj 350 | (defmacro recorded-fake* 351 | "This is the same as [[recorded-fake]] macro but for reuse in other macros. 352 | 353 | `form` must be passed from the parent macro in order to correctly detect position." 354 | ([ctx form] (-emit-fake-fn-call-with-position `-recorded-fake** ctx form)) 355 | ([ctx form config] (-emit-fake-fn-call-with-position `-recorded-fake** ctx form config)))) 356 | 357 | #?(:clj 358 | (defmacro recorded-fake 359 | "Creates a fake function whose calls will be recorded. 360 | 361 | Config is a vector of pairs: `[args-matcher1 fn-or-value1 args-matcher2 fn-or-value2 ...]` 362 | 363 | Ultimatelly, the created function must be marked checked (see [[mark-checked]]); otherwise, [[self-test-unchecked-fakes]] will fail. 364 | 365 | Use [[recorded-fake*]] in case this macro is called from another macro." 366 | ([ctx] `(recorded-fake* ~ctx ~&form)) 367 | ([ctx config] `(recorded-fake* ~ctx ~&form ~config)))) 368 | 369 | (defn ^:no-doc -recorded-fake-as* 370 | [ctx position k config] 371 | {:pre [ctx]} 372 | (-recorded-as ctx position k (-config->fn config))) 373 | 374 | #?(:clj 375 | (defmacro ^:no-doc -recorded-fake-as 376 | "The same as [[recorded-fake]], but fake will be stored into context by specified key k instead of its value. 377 | 378 | E.g. it is used in [[reify-fake]] in order to emulate recording calls on the protocol method. 379 | `form` must be passed if this macro is called from another macro in order to correctly determine position." 380 | ([ctx form k] (-emit-fake-fn-call-with-position `-recorded-fake-as* ctx form k `default-fake-config)) 381 | ([ctx form k config] (-emit-fake-fn-call-with-position `-recorded-fake-as* ctx form k config)))) 382 | 383 | (defn calls 384 | "For the specified recorded fake returns a vector of its calls. 385 | Returned vector is ordered by call time: the earliest call will be at the head. 386 | 387 | A call is a map with keys: `:args`, `:return-value`. 388 | 389 | If fake is not specified then it will return all the calls recorded in the context: 390 | 391 | ```clj 392 | [[recorded-fake1 call1] 393 | [recorded-fake2 call2] 394 | ...] 395 | ```" 396 | ([ctx] (:calls @ctx)) 397 | ([ctx f] 398 | {:pre [ctx (-recorded? ctx f)]} 399 | (->> (:calls @ctx) 400 | (filter #(= f (first %))) 401 | (mapv #(second %))))) 402 | 403 | ;;;;;;;;;;;;;;;;;;;;;;;; Protocol fakes 404 | (defn ^:no-doc -register-obj-id 405 | [ctx obj id] 406 | {:pre [ctx obj id]} 407 | (swap! ctx assoc-in [:object-ids obj] id)) 408 | 409 | (defn ^:no-doc -obj->id 410 | "Finds id for reified protocol instance. Returns nil if not found." 411 | [ctx obj] 412 | {:pre [ctx obj]} 413 | (get (:object-ids @ctx) obj)) 414 | 415 | (defn ^:no-doc -method-hash 416 | "Generates a unique hash from specified protocol instance id and function." 417 | [id f] 418 | {:pre [id f]} 419 | [id f]) 420 | 421 | (defn method 422 | "Returns a hash-key by which calls are recorded for the specified fake protocol instance method. 423 | Can be used in functions which take recorded fake as an input." 424 | [ctx obj f] 425 | {:pre [ctx obj (or (fn? f) (string? f))]} 426 | (-method-hash (-obj->id ctx obj) f)) 427 | 428 | (defn ^:no-doc -with-any-this-arg-config 429 | "Applies -with-any-this-arg to all args-matchers in config. Returns a new config." 430 | [config] 431 | {:pre [(vector? config)] 432 | :post [#(vector? %)]} 433 | (into [] 434 | (map-indexed (fn [i x] 435 | (if (even? i) 436 | (-with-any-this-arg x) 437 | x)) 438 | config))) 439 | 440 | #?(:clj 441 | (defn ^:no-doc -add-any-first-arg-into-matchers 442 | "In config map kinda adds an additional first matcher to match any 'this' param. 443 | Before: 444 | [matcher1 fn1 445 | matcher2 fn1] 446 | 447 | After: 448 | [(fn [args] (apply matcher1 (rest args))) fn1 449 | ...] 450 | 451 | Nil config is bypassed." 452 | [config] 453 | (when-not (nil? config) 454 | `(-with-any-this-arg-config ~config)))) 455 | 456 | #?(:clj 457 | (defn ^:no-doc -looks-like-method-spec? 458 | [x] 459 | (list? x))) 460 | 461 | #?(:clj 462 | (defn ^:no-doc -parse-specs 463 | "Returns a map: protocol -> method-specs" 464 | [specs] 465 | (loop [ret {} 466 | specs specs] 467 | (if (seq specs) 468 | (recur (assoc ret (first specs) (take-while -looks-like-method-spec? (next specs))) 469 | (drop-while -looks-like-method-spec? (next specs))) 470 | ret)))) 471 | 472 | #?(:clj 473 | (defn ^:no-doc -emit-method-full-sym 474 | "Infers fully qualified method symbol." 475 | [env protocol-sym method-sym] 476 | (if (m/-cljs-env? env) 477 | ; ClojureScript 478 | (let [protocol-var (r/-cljs-resolve env protocol-sym)] 479 | (symbol (namespace (:name protocol-var)) (name method-sym))) 480 | 481 | ; Clojure 482 | (let [protocol-var (resolve protocol-sym) 483 | ns (if (var? protocol-var) 484 | (str (:ns (meta protocol-var))))] 485 | (symbol ns (name method-sym)))))) 486 | 487 | #?(:clj 488 | (defn ^:no-doc -emit-imp-value 489 | [form env ctx obj-id-sym protocol-sym 490 | method-sym fake-type config] 491 | (let [method-full-sym (-emit-method-full-sym env protocol-sym method-sym) 492 | method-hash `(-method-hash 493 | ~obj-id-sym 494 | ; if compiler cannot resolve a symbol then hash by its string repr instead of the value 495 | ; if resolved symbol is really not a protocol/class it will fail later on reify macro expansion 496 | ~(if (r/-resolves? env method-full-sym) 497 | method-full-sym 498 | (str method-sym))) 499 | config (-add-any-first-arg-into-matchers config)] 500 | ; Descriptions are not set for :optional-fake and :-nice-fake because there's no need yet. 501 | (condp = fake-type 502 | :optional-fake 503 | (if config 504 | `(optional-fake ~ctx ~config) 505 | `(optional-fake ~ctx)) 506 | 507 | :fake 508 | `(let [fake# (fake* ~ctx ~form ~config)] 509 | (-set-desc ~ctx fake# ~(str protocol-sym ", " method-sym)) 510 | fake#) 511 | 512 | :recorded-fake 513 | `(do 514 | (-set-desc ~ctx ~method-hash ~(str protocol-sym ", " method-sym)) 515 | ~(if config 516 | `(-recorded-fake-as ~ctx ~form ~method-hash ~config) 517 | `(-recorded-fake-as ~ctx ~form ~method-hash))) 518 | 519 | :-nice-fake 520 | `(optional-fake ~ctx default-fake-config) 521 | 522 | (assert nil (str "Unknown fake type specified: " fake-type)))))) 523 | 524 | #?(:clj 525 | (defn ^:no-doc -remove-meta 526 | [x] 527 | (with-meta x nil))) 528 | 529 | #?(:clj 530 | (defn ^:no-doc -with-hint 531 | "hint can be a symbol (e.g. interop.InterfaceFixture) or a string (e.g. '[Ljava.lang.Integer;' or 'boolean'). 532 | See http://asymmetrical-view.com/2009/07/02/clojure-primitive-arrays.html and comments there." 533 | [hint sym] 534 | (with-meta sym {:tag hint}))) 535 | 536 | #?(:clj 537 | (defn ^:no-doc -arg 538 | "Constructs a globally unique arg symbol from the specified name. 539 | Without using this helper emitted arglists can have duplicated args and not allowed symbols. 540 | No type hints will be added." 541 | [name] 542 | (-> name 543 | ; sanitize in case Java name is passed in (e.g. java.lang.CharSequence) 544 | (string/replace #"\W" "-") 545 | (gensym)))) 546 | 547 | #?(:clj 548 | (defn ^:no-doc -arglist 549 | [arglist] 550 | (mapv -arg arglist))) 551 | 552 | #?(:clj 553 | (defn ^:no-doc -arglist-with-hints 554 | "Generates a method arglist with hints for the given interface. 555 | parameter-types does not include `this` arg." 556 | [interface-sym parameter-types] 557 | (into [(-with-hint interface-sym (-arg 'this))] 558 | (for [type parameter-types] 559 | (let [type-name (.getName type)] 560 | (-with-hint type-name (-arg type-name))))))) 561 | 562 | #?(:clj 563 | (defn ^:no-doc -method-signatures 564 | "Given a protocol and a method returns all allowed method signatures: 565 | ({:method-sym ... 566 | :arglist [...]} ...) 567 | 568 | If protocol is a Java Object or interface then method sym and args will be hinted (which is needed 569 | to support overloaded methods)." 570 | [env protocol-sym method-sym] 571 | (if-let [protocol (r/-resolve-protocol-with-specs env protocol-sym)] 572 | ; protocol 573 | (let [methods (r/-protocol-methods env protocol) 574 | method (-find-first #(= method-sym (r/-protocol-method-name env %)) methods) 575 | arglists (map -arglist (r/-protocol-method-arglist env method))] 576 | (for [arglist arglists] 577 | {:method-sym method-sym 578 | :arglist arglist})) 579 | 580 | ; not a protocol 581 | (if (m/-cljs-env? env) 582 | ; ClojureScript - something went wrong 583 | (assert nil (str "Unknown protocol: " protocol-sym)) 584 | 585 | ; Clojure - Object, Java interface or error 586 | (let [all-methods (r/-interface-or-object-methods env protocol-sym) 587 | methods (filter #(= (:name %) (str method-sym)) all-methods) 588 | overloaded? (> (count methods) 1)] 589 | (for [{:keys [return-type parameter-types]} methods] 590 | (if overloaded? 591 | {:method-sym (-with-hint (.getName return-type) method-sym) 592 | :arglist (-arglist-with-hints protocol-sym parameter-types)} 593 | {:method-sym method-sym 594 | :arglist (-arglist (into ['this] parameter-types))}))))))) 595 | 596 | #?(:clj 597 | (defn ^:no-doc -method-imp 598 | "Generates an implementation for the specified method. 599 | {:imp-sym ... 600 | :imp-value ... 601 | :signatures ({:method-sym ... :arglist ...} ...)} 602 | 603 | If protocol is a Java interface then method signature(s) will have type hints (it is needed 604 | to support overloaded methods)." 605 | [form env ctx obj-id-sym protocol-sym method-spec] 606 | (let [imp-sym (gensym "imp")] 607 | (if (and (m/-cljs-env? env) 608 | (= protocol-sym 'Object)) 609 | ; edge case: allow implementing any new methods for Object protocol in ClojureScript 610 | (let [[method-sym arglist-hint fake-type config] method-spec 611 | ; validate arglist and add first 'this' arg 612 | arglist (do 613 | (assert (vector? arglist-hint) 614 | (str "Vector arglist expected to be specified for an Object method: " protocol-sym "/" method-sym 615 | ". Actual value: " (pr-str arglist-hint))) 616 | (into ['this] arglist-hint)) 617 | imp-value (-emit-imp-value 618 | form env ctx 619 | obj-id-sym 620 | protocol-sym 621 | method-sym fake-type config)] 622 | {:imp-sym imp-sym 623 | :imp-value imp-value 624 | :signatures [{:method-sym method-sym :arglist arglist}]}) 625 | 626 | ; protocol, Java interface or Clojure Object 627 | (let [[method-sym fake-type config] method-spec 628 | imp-value (-emit-imp-value 629 | form env ctx 630 | obj-id-sym 631 | protocol-sym 632 | method-sym fake-type config) 633 | signatures (-method-signatures env protocol-sym method-sym)] 634 | (assert (seq signatures) (str "Unknown method: " protocol-sym "/" method-sym)) 635 | 636 | {:imp-sym imp-sym 637 | :imp-value imp-value 638 | :signatures signatures}))))) 639 | 640 | #?(:clj 641 | (defn ^:no-doc -method-imps-for-protocol 642 | "Returns a list of maps: 643 | ({:imp-sym ... 644 | :imp-value ... 645 | :signatures ...} ...)" 646 | [form env ctx obj-id-sym protocol-sym method-specs] 647 | (map (partial -method-imp form env ctx obj-id-sym protocol-sym) method-specs))) 648 | 649 | #?(:clj 650 | (defn ^:no-doc -generate-protocol-method-imps 651 | "Produces a helper map: 652 | {protocol-sym -> [{:imp-sym ... 653 | :imp-value ... 654 | :signatures ...} ...] ...}" 655 | [form env ctx obj-id-sym protocol-method-specs] 656 | (into {} 657 | (map (fn [[protocol-sym method-specs]] 658 | (vector protocol-sym 659 | (-method-imps-for-protocol form env ctx obj-id-sym protocol-sym method-specs))) 660 | protocol-method-specs)))) 661 | 662 | #?(:clj 663 | (defn ^:no-doc -emit-imp-bindings 664 | "Emits a list of let bindings with implementations for reified methods: 665 | [imp-sym1 imp-value1 666 | imp-sym2 imp-value2 ...]" 667 | [protocol-method-imps] 668 | (mapcat #(vector (:imp-sym %) (:imp-value %)) 669 | (flatten (vals protocol-method-imps))))) 670 | 671 | #?(:clj 672 | (defn ^:no-doc -emit-method-specs 673 | [{:keys [imp-sym signatures] :as _method-imp}] 674 | (map (fn [{:keys [method-sym arglist]}] 675 | `(~method-sym ~arglist 676 | (~imp-sym ~@(map -remove-meta arglist)))) 677 | signatures))) 678 | 679 | #?(:clj 680 | (defn ^:no-doc -emit-protocol-specs 681 | [[protocol-sym method-imps]] 682 | (into [protocol-sym] 683 | (mapcat -emit-method-specs method-imps)))) 684 | 685 | #?(:clj 686 | (defn ^:no-doc -emit-specs 687 | "Emits final specs for reify macro." 688 | [protocol-method-imps] 689 | (mapcat -emit-protocol-specs protocol-method-imps))) 690 | 691 | #?(:clj 692 | (defn ^:no-doc -nice-specs 693 | "Generates nice specs for all methods from the specified protocol. 694 | Does nothing for Java interfaces and unresolved symbols." 695 | [env protocol-sym] 696 | (when-let [protocol (r/-resolve-protocol-with-specs env protocol-sym)] 697 | (let [methods (r/-protocol-methods env protocol) 698 | nice-specs (map #(vector (r/-protocol-method-name env %) :-nice-fake) methods)] 699 | nice-specs)))) 700 | 701 | #?(:clj 702 | (defn ^:no-doc -not-yet-in-specs? 703 | [specs [nice-method-sym :as _spec]] 704 | (nil? 705 | (-find-first (fn [[method-sym :as _spec]] 706 | (= method-sym nice-method-sym)) 707 | specs)))) 708 | 709 | #?(:clj 710 | (defn ^:no-doc -add-nice-specs-for-protocol 711 | [env [protocol-sym parsed-specs :as _parsed-spec]] 712 | (let [nice-specs (-nice-specs env protocol-sym) 713 | auto-specs (filter (partial -not-yet-in-specs? parsed-specs) nice-specs) 714 | new-specs (concat parsed-specs auto-specs)] 715 | [protocol-sym new-specs]))) 716 | 717 | #?(:clj 718 | (defn ^:no-doc -add-nice-specs 719 | "For all methods which are not explicitly defined adds a 'nice' default spec." 720 | [env parsed-specs] 721 | (map (partial -add-nice-specs-for-protocol env) parsed-specs))) 722 | 723 | #?(:clj 724 | (defmacro ^:no-doc -reify-fake* 725 | [ctx form env debug? nice? & specs] 726 | (let [obj-id-sym (gensym "obj-id") 727 | obj-sym (gensym "obj") 728 | parsed-specs (-parse-specs specs) 729 | protocol-method-specs (if nice? 730 | (-add-nice-specs env parsed-specs) 731 | parsed-specs) 732 | protocol-method-imps (-generate-protocol-method-imps form env ctx obj-id-sym protocol-method-specs) 733 | result `(let [~obj-id-sym (gensym "obj-id") 734 | ~@(-emit-imp-bindings protocol-method-imps) 735 | ~obj-sym (reify ~@(-emit-specs protocol-method-imps))] 736 | (-register-obj-id ~ctx ~obj-sym ~obj-id-sym) 737 | ~obj-sym)] 738 | (when debug? 739 | ;(set! *print-meta* true) 740 | (m/PP result) 741 | (m/PP "--") 742 | (set! *print-meta* false)) 743 | result))) 744 | 745 | #?(:clj 746 | (defmacro reify-fake* 747 | "The same as [[reify-fake]] but to be used inside macros. 748 | 749 | `form` is needed to correctly determine fake positions, `env` is needed to determine target language." 750 | [ctx form env & specs] 751 | `(-reify-fake* ~ctx ~form ~env false false ~@specs))) 752 | 753 | #?(:clj 754 | (defmacro ^:no-doc -reify-fake-debug* 755 | "The same as reify-fake* but with logging to console turned on. Was added for debugging." 756 | [ctx form env & specs] 757 | `(-reify-fake* ~ctx ~form ~env true false ~@specs))) 758 | 759 | #?(:clj 760 | (defmacro reify-nice-fake* 761 | "The same as [[reify-nice-fake]] but to be used inside macros. 762 | 763 | `form` is needed to correctly determine fake positions, `env` is needed to determine target language." 764 | [ctx form env & specs] 765 | `(-reify-fake* ~ctx ~form ~env false true ~@specs))) 766 | 767 | #?(:clj 768 | (defmacro reify-fake 769 | "Works similarly to `reify` macro, but implements methods in terms of fake functions. 770 | Created instance will raise an exception on calling protocol method which is not defined. 771 | 772 | Supported fake types: `:optional-fake`, `:fake`, `:recorded-fake`. 773 | 774 | Syntax example: 775 | 776 | ```clj 777 | (reify-fake my-context 778 | protocol-or-interface-or-Object 779 | [method1 :optional-fake [any 123]] 780 | 781 | protocol-or-interface-or-Object 782 | [method2 :recorded-fake] 783 | 784 | ; reification of Object with arbitrary methods works only in ClojureScript, note how arglist must be explicitly provided: 785 | Object 786 | [new-method3 [x y z] :recorded-fake]) 787 | ```" 788 | [ctx & specs] 789 | `(reify-fake* ~ctx ~&form ~&env ~@specs))) 790 | 791 | #?(:clj 792 | (defmacro reify-nice-fake 793 | "Works similarly to [[reify-fake]], but automatically generates `:optional-fake` 794 | implementations for methods which are not explicitly defined by user. 795 | 796 | It cannot yet automatically fake Java interface and `Object` methods." 797 | [ctx & specs] 798 | `(reify-nice-fake* ~ctx ~&form ~&env ~@specs))) 799 | 800 | ;;;;;;;;;;;;;;;;;;;;;;;; Self-tests 801 | (defn ^:no-doc -fake->str 802 | [ctx fake-type f] 803 | (let [p (-position ctx f) 804 | base-str (str fake-type " from " (:file p) ", " (:line p) ":" (:column p)) 805 | desc (get (:fake-descs @ctx) f)] 806 | (if desc 807 | (str base-str " (" desc ")") 808 | base-str))) 809 | 810 | (defn self-test-unused-fakes 811 | "Raises an exception when some fake was never called after its creation." 812 | [ctx] 813 | {:pre [ctx]} 814 | (when-let [descriptions (not-empty (map (partial -fake->str ctx "non-optional fake") 815 | (:unused-fakes @ctx)))] 816 | (throw (ex-info (str "Self-test: no call detected for:\n" 817 | (string/join "\n" descriptions)) 818 | {})))) 819 | 820 | (defn self-test-unchecked-fakes 821 | "Raises an exception if some recorded fake was never marked checked, i.e. you forgot to assert its calls." 822 | [ctx] 823 | {:pre [ctx]} 824 | (when-let [descriptions (not-empty (map (partial -fake->str ctx "recorded fake") 825 | (:unchecked-fakes @ctx)))] 826 | (throw (ex-info (str "Self-test: no check performed on:\n" 827 | (string/join "\n" descriptions)) 828 | {})))) 829 | 830 | (defn self-test 831 | "Runs all available self-tests." 832 | [ctx] 833 | (self-test-unchecked-fakes ctx) 834 | (self-test-unused-fakes ctx)) 835 | 836 | ;;;;;;;;;;;;;;;;;;;;;;;; Assertions 837 | (defn ^:no-doc -was-called-times 838 | "Checks that function was called the specified number of times." 839 | [ctx f calls-count-pred calls-count-str] 840 | (let [calls-count (count (calls ctx f))] 841 | (if (calls-count-pred calls-count) 842 | true 843 | (throw (ex-info (str "Function was not called the expected number of times. Expected: " calls-count-str ". " 844 | "Actual: " calls-count ".") 845 | {}))))) 846 | 847 | (defn ^:no-doc -was-matched 848 | "Checks that there was a call with the specified args. 849 | Optionally: checks that strictly one call satisfies the args matcher." 850 | [ctx f args-matcher strictly-once?] 851 | (let [f-calls (calls ctx f) 852 | matched-calls (filter #(args-match? args-matcher (:args %)) 853 | f-calls) 854 | matched-calls-count (count matched-calls)] 855 | (cond 856 | (zero? matched-calls-count) 857 | (throw (ex-info (str "Function was never called with the expected args.\n" 858 | "Args matcher: " (args-matcher->str args-matcher) ".\n" 859 | "Actual calls:\n" (with-out-str (pprint/pprint f-calls))) 860 | {})) 861 | 862 | (and strictly-once? (> matched-calls-count 1)) 863 | (throw (ex-info (str "More than one call satisfies the provided args matcher.\n" 864 | "Args matcher: " (args-matcher->str args-matcher) ".\n" 865 | "Matched calls:\n" (with-out-str (pprint/pprint (vec matched-calls)))) 866 | {})) 867 | 868 | :else true))) 869 | 870 | (defn was-called-once 871 | "Checks that recorded fake was called strictly once and that the call was with the specified args. 872 | Returns true or raises an exception." 873 | [ctx f args-matcher] 874 | {:pre [ctx (-recorded? ctx f) (satisfies? ArgsMatcher args-matcher)]} 875 | (mark-checked ctx f) 876 | (-was-called-times ctx f #(= % 1) "1") 877 | (-was-matched ctx f args-matcher false)) 878 | 879 | (defn was-called 880 | "Checks that recorded fake was called at least once with the specified args. 881 | Returns true or raises an exception." 882 | [ctx f args-matcher] 883 | {:pre [ctx (-recorded? ctx f) (satisfies? ArgsMatcher args-matcher)]} 884 | (mark-checked ctx f) 885 | (-was-called-times ctx f #(> % 0) "> 0") 886 | (-was-matched ctx f args-matcher false)) 887 | 888 | (defn was-matched-once 889 | "Checks that recorded fake was called at least once and only a single call satisfies the provided args matcher. 890 | Returns true or raises an exception." 891 | [ctx f args-matcher] 892 | {:pre [ctx (-recorded? ctx f) (satisfies? ArgsMatcher args-matcher)]} 893 | (mark-checked ctx f) 894 | (-was-called-times ctx f #(> % 0) "> 0") 895 | (-was-matched ctx f args-matcher true)) 896 | 897 | (defn was-not-called 898 | "Checks that recorded fake was never called. 899 | Returns true or raises an exception." 900 | [ctx f] 901 | {:pre [ctx (-recorded? ctx f)]} 902 | (mark-checked ctx f) 903 | (let [f-calls (calls ctx f)] 904 | (or (empty? f-calls) 905 | (throw (ex-info (str "Function is expected to be never called. Actual calls:\n" (pr-str f-calls) ".") {}))))) 906 | 907 | (defn ^:no-doc -call-matches? 908 | [f args-matcher [call-f {:keys [args]} :as _call]] 909 | (and (= f call-f) 910 | (args-match? args-matcher args))) 911 | 912 | (defn were-called-in-order 913 | "Checks that recorded fakes were called in the specified order with the specified args. 914 | It does not check that there were no other calls. 915 | 916 | Syntax: 917 | ``` 918 | (were-called-in-order ctx 919 | f1 args-matcher1 920 | f2 args-matcher2 921 | ...) 922 | ``` 923 | 924 | Returns true or raises an exception." 925 | [ctx & fns-and-matchers] 926 | {:pre [ctx 927 | (pos? (count fns-and-matchers)) 928 | (even? (count fns-and-matchers)) 929 | (every? (partial -recorded? ctx) (-take-nth-from-group 1 2 fns-and-matchers)) 930 | (every? #(satisfies? ArgsMatcher %) (-take-nth-from-group 2 2 fns-and-matchers))]} 931 | (let [fn-matcher-pairs (partition 2 fns-and-matchers)] 932 | (doseq [[f _] fn-matcher-pairs] 933 | (mark-checked ctx f)) 934 | 935 | ; for each provided pair 936 | (loop [unchecked-pairs fn-matcher-pairs 937 | unchecked-calls (calls ctx) 938 | step 1] 939 | (when-let [[f args-matcher] (first unchecked-pairs)] 940 | ; find matched call 941 | (let [[matched-call rest-unchecked-calls] (-find-and-take-after (partial -call-matches? f args-matcher) 942 | unchecked-calls)] 943 | ; not found error 944 | (when (nil? matched-call) 945 | (throw (ex-info (str "Could not find a call satisfying step #" step 946 | ":\n" (-fake->str ctx "recorded fake" f) 947 | "\nargs matcher: " (args-matcher->str args-matcher)) {}))) 948 | 949 | ; otherwise, check next pair 950 | (recur (rest unchecked-pairs) 951 | rest-unchecked-calls 952 | (inc step)))))) 953 | true) 954 | 955 | ;;;;;;;;;;;;;;;;;;;;;;;; Assertions for protocol methods 956 | (defn method-was-called-once 957 | "[[was-called-once]] for protocol method fakes." 958 | [ctx f obj args-matcher] 959 | {:pre [ctx f obj (satisfies? ArgsMatcher args-matcher)]} 960 | (was-called-once ctx (method ctx obj f) (-with-any-this-arg args-matcher))) 961 | 962 | (defn method-was-called 963 | "[[was-called]] for protocol method fakes." 964 | [ctx f obj args-matcher] 965 | {:pre [ctx f obj (satisfies? ArgsMatcher args-matcher)]} 966 | (was-called ctx (method ctx obj f) (-with-any-this-arg args-matcher))) 967 | 968 | (defn method-was-matched-once 969 | "[[was-matched-once]] for protocol method fakes." 970 | [ctx f obj args-matcher] 971 | {:pre [ctx f obj (satisfies? ArgsMatcher args-matcher)]} 972 | (was-matched-once ctx (method ctx obj f) (-with-any-this-arg args-matcher))) 973 | 974 | (defn method-was-not-called 975 | "[[was-not-called]] for protocol method fakes." 976 | [ctx f obj] 977 | {:pre [ctx f obj]} 978 | (was-not-called ctx (method ctx obj f))) 979 | 980 | (defn methods-were-called-in-order 981 | "[[were-called-in-order]] for protocol method fakes. 982 | 983 | Syntax: 984 | ``` 985 | (methods-were-called-in-order ctx 986 | f1 obj1 args-matcher1 987 | f2 obj2 args-matcher2 988 | ...) 989 | ```" 990 | [ctx & fns-objs-and-matchers] 991 | {:pre [ctx 992 | (pos? (count fns-objs-and-matchers)) 993 | (zero? (rem (count fns-objs-and-matchers) 3)) 994 | (every? #(not (nil? %)) (-take-nth-from-group 1 3 fns-objs-and-matchers)) 995 | (every? #(not (nil? %)) (-take-nth-from-group 2 3 fns-objs-and-matchers)) 996 | (every? #(satisfies? ArgsMatcher %) (-take-nth-from-group 3 3 fns-objs-and-matchers)) 997 | ]} 998 | (let [fns-and-matchers (mapcat (fn [[f obj args-matcher]] 999 | [(method ctx obj f) (-with-any-this-arg args-matcher)]) 1000 | (partition 3 fns-objs-and-matchers))] 1001 | (apply were-called-in-order ctx fns-and-matchers))) 1002 | 1003 | ;;;;;;;;;;;;;;;;;;;;;;;; Monkey patching 1004 | (defn ^:no-doc -save-original-val! 1005 | [ctx a-var] 1006 | {:pre [ctx a-var]} 1007 | (when-not (contains? (:original-vals @ctx) a-var) 1008 | (swap! ctx assoc-in [:original-vals a-var] @a-var))) 1009 | 1010 | (defn ^:no-doc -save-unpatch! 1011 | [ctx a-var unpatch-fn] 1012 | {:pre [ctx a-var (fn? unpatch-fn)]} 1013 | (swap! ctx assoc-in [:unpatches a-var] unpatch-fn)) 1014 | 1015 | (defn original-val 1016 | "Given a patched variable returns its original value." 1017 | [ctx a-var] 1018 | {:pre [ctx a-var]} 1019 | (assert (contains? (:original-vals @ctx) a-var) "Specified var is not patched") 1020 | (get (:original-vals @ctx) a-var)) 1021 | 1022 | #?(:clj 1023 | (defmacro ^:no-doc -set-var! 1024 | "Portable var set. ClojureScript version expects passed variable to be a pair, e.g. `(var foo)`." 1025 | [a-var val] 1026 | (if (m/-cljs-env? &env) 1027 | ; ClojureScript 1028 | (let [[_ var-symbol] a-var] 1029 | `(set! ~var-symbol ~val)) 1030 | 1031 | ; Clojure 1032 | `(alter-var-root ~a-var (constantly ~val))))) 1033 | 1034 | #?(:clj 1035 | (defmacro patch! 1036 | "Changes variable value in all threads." 1037 | [ctx var-expr val] 1038 | `(do 1039 | (-save-original-val! ~ctx ~var-expr) 1040 | (-save-unpatch! ~ctx 1041 | ~var-expr 1042 | #(-set-var! ~var-expr (original-val ~ctx ~var-expr))) 1043 | (-set-var! ~var-expr ~val)))) 1044 | 1045 | (defn unpatch! 1046 | "Restores original variable value." 1047 | [ctx a-var] 1048 | {:pre [ctx a-var]} 1049 | (assert (contains? (:unpatches @ctx) a-var) "Specified var is not patched") 1050 | ((get (:unpatches @ctx) a-var))) 1051 | 1052 | (defn unpatch-all! 1053 | "Restores original values for all the variables patched in the specified context." 1054 | [ctx] 1055 | (doseq [[_var unpatch-fn] (:unpatches @ctx)] 1056 | (unpatch-fn))) 1057 | 1058 | ;;;;;;;;;;;;;;;;;;;;;;;; Utils 1059 | (defn cyclically 1060 | "Returns a function that: 1061 | 1062 | 1. takes any number of arguments; 1063 | 2. on each call returns the next value from `coll`, cyclically." 1064 | [coll] 1065 | (let [vals (atom (cycle coll))] 1066 | (fn cyclical-fn [& _args] 1067 | (let [result (first @vals)] 1068 | (swap! vals next) 1069 | result)))) --------------------------------------------------------------------------------