Namespaces
The public API is split into two namespaces:
clj-fakes.core
clj-fakes.context
These namespaces contain almost the same set of members. The difference
is that core
uses an implicit context and the context
namespace
functions require an explicit context argument.
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
).
The private/internal API uses a -
prefix and should not be used (e.g. -this-is-some-private-thing
).
Context
Context is an object which stores all the information about created fakes (recorded calls, positions in code, etc.). All fakes have to be created inside some context.
To create a new context use clj-fakes.context/context
:
; explicit context
(let [ctx (fc/context)
foo (fc/recorded-fake ctx)]
; ...
)
Alternatively a new context can be created with
clj-fakes.core/with-fakes
macro:
; implicit context
(f/with-fakes
; note that now fake is created using a macro from core ns
(let [foo (f/recorded-fake)]
; ...
))
This approach is preferable since it requires less typing, automatically unpatches all patched vars and executes self-tests.
Internally with-fakes
relies on a public dynamic variable *context*
which can be
used in your own helper functions.
Function Fakes
Fake is a function which returns canned values on matched arguments and can optionally record its calls. It can be used to define and assert a behavior of a functional dependency of an SUT (system under test).
Fake
A regular fake function can be created using a macro:
(f/fake config)
(fc/fake ctx config)
Config is a vector which defines which values to return for different arguments:
(let [foo (f/fake [[1 2] "foo"
[3 4 5] "bar"])]
(foo 1 2) ; => "foo"
(foo 3 4 5)) ; => "bar"
If passed arguments cannot be matched using specified config then the exception will be raised:
(foo 100 200) ; => raises "Unexpected args are passed into fake: (100 200) ..."
A fake is assumed to be called at least once inside the context; otherwise, self-test exception will be raised. In such case user should either modify a test, an SUT or consider using an optional fake:
(f/with-fakes
(f/fake [[] nil])) ; => raises "Self-test: no call detected for: non-optional fake ..."
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, instead use recorded fakes with explicit assertions. Self-tests are more about checking usefulness of provided preconditions than about testing expected behavior.
Optional Fake
(f/optional-fake [config])
(fc/optional-fake ctx [config])
It works the same as a regular fake but is not expected to be always called in the context:
(f/with-fakes
(f/optional-fake [[1 2] 3])) ; => ok, self-test will pass
Such fakes should be used to express the intent of the test writer, for example, when you have to provide a dependency to an SUT, but this dependency is not really related to the test case:
(defn process-payments
"Processor requires a logger."
[data logger]
{:pre [(fn? logger)]}
; ...
)
(deftest good-payments-are-processed-without-error
(f/with-fakes
(let [; ...
; we are not interested in how logger is going to be used, just stub it and forget
fake-logger (f/optional-fake)]
(is (= :success (process-payments good-payments fake-logger))))))
As you may have noticed, config
argument can be omitted.
In such case fake will be created with default-fake-config
which allows any arguments to be passed on invocation.
Recorded Fake
Invocations of this fake are recorded so that they can later be asserted:
(f/recorded-fake [config])
(fc/recorded-fake ctx [config])
Use calls
function in order to get all recorded invocations for the specified
recorded fake.
It can also return all the recorded calls in the context if fake is not specified:
(let [foo (f/recorded-fake [[(f/arg integer?) (f/arg integer?)] #(+ %1 %2)])
bar (f/recorded-fake [[(f/arg integer?) (f/arg integer?)] #(* %1 %2)])]
(foo 1 2)
(bar 5 6)
(foo 7 8)
(f/calls foo)
; => [{:args [1 2] :return-value 3}
; {:args [7 8] :return-value 15}]
(f/calls)
; => [[foo {:args [1 2] :return-value 3}]
; [bar {:args [5 6] :return-value 30}]
; [foo {:args [7 8] :return-value 15}]]
)
Recorded fake must be checked using one of the assertions provided by the framework or
be marked as checked explicitly using mark-checked
function;
otherwise, self-test will raise an exception:
(f/with-fakes
(f/recorded-fake)) ; => raises "Self-test: no check performed on: recorded fake ..."
(f/with-fakes
(let [foo (f/recorded-fake)]
(foo)
(is (f/was-called foo [])))) ; => ok, self-test will pass
(f/with-fakes
(f/mark-checked (f/recorded-fake))) ; => ok, self-test will pass
Custom Macros
In your own reusable macros you should use fake*/recorded-fake*
instead of fake/recorded-fake
:
(f/fake* form config)
(fc/fake* ctx form config)
(f/recorded-fake* form [config])
(fc/recorded-fake* ctx form [config])
In other words, your macro must explicitly provide &form
to framework macros;
otherwise, due to implementation details, framework will not be
able to correctly determine fake function line numbers which is crucial for debugging.
The framework will warn you if you accidentally use the version without asterisk in your macro.
Fake Configuration
Fake config should contain pairs of args matcher and return value:
[args-matcher1 fn-or-value1
args-matcher2 fn-or-value2 ...]
On fake invocation matchers will be tested from top to bottom and on the first match the specified value will be returned. If return value is a function than it will be called with passed arguments to generate the return value at runtime:
(let [foo (f/fake [[1 2] 100
[3 4] #(+ %1 %2)
[5 6] (fn [_ _] (throw (ex-info "wow" {})))])]
(foo 1 2) ; => 100
(foo 3 4) ; => 7
(foo 5 6)) ; => raises "wow" exception
There's one built-in config in the framework:
fc/default-fake-config
It accepts any number of arguments and returns a new unique
instance of type FakeReturnValue
on each call.
It is used by optional-fake
and recorded-fake
functions by default (i.e. when user
doesn't specify the config explicitly).
Helpers
(f/cyclically coll)
(fc/cyclically coll)
This function can be used to implement iterator-style stubbing when a fake returns a different value on each call:
(let [get-weekday (f/fake [["My event"] (f/cyclically [:monday :tuesday :wednesday])])]
(is (= :monday (get-weekday "My event")))
(is (= :tuesday (get-weekday "My event")))
(is (= :wednesday (get-weekday "My event")))
(is (= :monday (get-weekday "My event"))))
Argument Matching
Every arguments matcher must implement an fc/ArgsMatcher
protocol:
(defprotocol ArgsMatcher
(args-match? [this args] "Should return true or false.")
(args-matcher->str [this] "Should return a string for debug messages."))
In most cases you won't need to create instances of this protocol manually because framework provides vector matchers which are useful in most cases.
Vector Matcher
Vector matchers were already used all other this guide.
Each vector element can be an expected value or an fc/ImplicitArgMatcher
instance:
[implicit-arg-matcher-or-exact-value1 implicit-arg-matcher-or-exact-value2 ...]
(defprotocol ImplicitArgMatcher
(arg-matches-implicitly? [this arg] "Should return true or false.")
(arg-matcher->str [this] "Should return a string for debug messages."))
It is not recommended to extend existing types with ImplicitArgMatcher
protocol;
instead, to make code more explicit and future-proof,
you should use an arg
"adapter" macro and pass it ArgMatcher
instances:
(let [foo (f/fake [[] "no args"
[[]] "empty vector"
[1 2] "1 2"
[(f/arg integer?) (f/arg integer?)] "two integers"
[(f/arg string?)] "string"])]
(foo) ; => "no args"
(foo []) ; => "empty vector"
(foo 1 2) ; => "1 2"
(foo 1 2 3) ; => exception: "Unexpected args are passed into fake: (1 2 3) ..."
(foo 100 200) ; => "two integers"
(foo "hey")) ; => "string"
As you can see, the framework already supports functional argument matchers which are implemented by extending function type like this:
(extend-type #?(:clj clojure.lang.Fn
:cljs function)
ArgMatcher
(arg-matches? [this arg]
(this arg)))
You are encouraged to define your own argument matchers in a similar way.
The framework also supports regex matchers (using
re-find
under the hood), for example: (f/arg #"abc.*")
.
any
(f/any _)
(fc/any _)
This special matcher always returns true
for any input arguments.
It can be used to match single and multiple arguments:
(let [foo (f/fake [[1 2] "1 2"
[f/any f/any f/any] "three args"
f/any "something else"])]
(foo) ; => "something else"
(foo 1) ; => "something else"
(foo 1 2) ; => "1 2"
(foo 1 2 3) ; => "three args"
(foo 1 2 3 4)) ; => "something else"
Protocol Fakes
Framework defines two new macros for reifying protocols using function fakes described earlier. So, for example, you can record and assert method calls on reified instances.
The "strict" reify-fake
macro is very similar to reify
; in particular,
created instance will raise an exception
on calling protocol method which is not defined.
On the other hand, reify-nice-fake
is able to automatically
generate optional-fake implementations for methods which are
not explicitly defined by user.
Which macro to use solely depends on your testing style. I'd recommend to use nice fakes whenever possible in order to make tests more compact and break less often on code changes.
There are some subtleties, so here's a table to give you an overview of which features are currently supported:
Feature | reify-fake |
reify-nice-fake |
---|---|---|
Fake protocol method (explicitly) | Yes | Yes |
Fake protocol method (auto) | - | Yes |
Fake Java interface method (explicitly) | Yes | Yes |
Fake Java interface method (auto) | - | No |
Fake Object method (explicitly) | Yes | Only in Clojure |
Fake Object method (auto) | - | No |
Object can be reified with any new methods | Only in ClojureScript | Only in ClojureScript |
Support overloaded methods | Yes | Yes |
Syntax
The syntax is very similar to the built-in reify
macro:
(f/reify-fake specs*)
(fc/reify-fake ctx specs*)
(f/reify-nice-fake specs*)
(fc/reify-nice-fake ctx specs*)
Each spec consists of the protocol or interface name followed by zero or more method fakes:
protocol-or-interface-or-Object
(method-name [arglist] fake-type [config])*
Available fake types:
:fake
(see Fake):optional-fake
(see Optional Fake):recorded-fake
(see Recorded Fake)
As with function fakes, config can be omitted for :optional-fake
and :recorded-fake
:
(defprotocol AnimalProtocol
(speak [this] [this name] [this name1 name2])
(eat [this food drink])
(sleep [this]))
(defprotocol FileProtocol
(save [this])
(scan [this]))
; ...
(f/reify-fake
p/AnimalProtocol
(sleep :fake [f/any "zzz"])
(speak :recorded-fake)
p/FileProtocol
(save :optional-fake)
java.lang.CharSequence
(charAt :recorded-fake [f/any \a]))
Although protocol methods always have a first this
argument,
method configs must not try to match this argument.
However, the return value function will receive all the arguments on invocation,
including this
:
(let [monkey (f/reify-fake p/AnimalProtocol
; config only matches |food| and |drink| arguments
; but return value function will get all 3 arguments on call
(eat :fake [[f/any f/any] #(str "ate " %2 " and drank " %3)]))]
(println (p/eat monkey "banana" "water"))) ; => ate banana and drank water
In ClojureScript it's possible to implement any methods under Object
protocol.
The framework supports this scenario but requires an arglist explicitly specified after the method name
(this
arg should be omitted):
(let [calculator (f/reify-fake Object
(sum [x y] :fake [[f/any f/any] #(+ %2 %3)])
(toString [] :optional-fake [[] "my calculator"]))]
(is (= 5 (.sum calculator 2 3)))
(is (= "my calculator" (str calculator))))
Calls & Assertions
In order to get and assert recorded method calls there's a helper function:
(f/method obj f)
(fc/method ctx obj f)
It can be used in combination with existing calls
and was-called-*
functions like this:
(f/with-fakes
(let [cow (f/reify-fake p/AnimalProtocol
(speak :recorded-fake [f/any "moo"]))]
(p/speak cow)
(println (f/calls (f/method cow p/speak))) ; => [{:args ..., :return-value moo}]
(is (f/was-called-once (f/method cow p/speak) [cow]))))
Notice how object name cow
is duplicated at the last line. In order to get
rid of such duplications there are additional method-*
assertions defined.
So the last expression can be rewritten like this:
(is (f/method-was-called-once p/speak cow []))
For the list of all available assertion functions see Assertions.
There's a quirk when Java interface or ClojureScript Object
method is faked: you will need to use its
string representation in method
/method-*
:
(let [foo (f/reify-fake clojure.lang.IFn
(invoke :recorded-fake))]
(foo 1 2 3)
(is (f/method-was-called "invoke" foo [1 2 3])))
Custom Macros
In your own reusable macros you should use reify-fake*/reify-nice-fake*
instead of reify-fake/reify-nice-fake
:
(f/reify-fake* form env specs*)
(fc/reify-fake* ctx form env specs*)
(f/reify-nice-fake* form env specs*)
(fc/reify-nice-fake* ctx form env specs*)
In other words, your macro must explicitly provide &form
and &env
to framework macros;
otherwise, due to implementation details, framework will not be
able to correctly determine fake method line numbers which is crucial for debugging.
For instance:
(defmacro my-reify-fake
[& specs]
`(f/reify-fake* ~&form ~&env ~@specs))
The framework will warn you if you accidentally use the version without asterisk in your macro.
Assertions
Framework provides several assertion functions for recorded fakes.
Each function either returns true
or raises an exception with additional details:
(f/was-called-once f args-matcher)
- checks that function was called strictly once and that the call was with the specified args.
(f/was-called f args-matcher)
- checks that function was called at least once with the specified args.
(f/was-matched-once f args-matcher)
- checks that function was called at least once and only a single call satisfies the provided args matcher.
(f/was-not-called f)
- checks that function was never called.
(f/were-called-in-order f1 args-matcher1 f2 args-matcher2 ...)
- checks that functions were called in specified order (but it doesn't guarantee there were no other calls).
The set of similar functions is defined for protocol methods:
(f/method-was-called-once f obj args-matcher)
(f/method-was-called f obj args-matcher)
(f/method-was-matched-once f obj args-matcher)
(f/method-was-not-called f obj)
(f/methods-were-called-in-order f1 obj1 args-matcher1 f2 obj2 args-matcher2 ...)
Of course, all these functions can be called with an explicit context, e.g.:
(fc/was-called-once ctx f args-matcher)
Self-tests
Framework can perform "self-tests" in order to inform a user early on that some fakes (including protocol method fakes) are potentially used inappropriately.
If you use with-fakes
macro then self-tests will be run automatically on exiting the block.
Otherwise, when explicit context is used, you have to invoke self-tests manually
using next function:
(fc/self-test ctx)
Each test can also be run manually using dedicated functions. Currently two types of self-tests are supported to identify:
- unused fakes
- unchecked fakes
Unused Fakes
(fc/self-test-unused-fakes ctx)
This function raises an exception when some fake was never called after its creation.
For example, this self-test comes in handy when SUT stops using a dependency which was faked in several test scenarios. In such case the framework will guide you in cleaning your test suite from the unused stubs.
Unchecked Fakes
(fc/self-test-unchecked-fakes ctx)
This self-test raises an exception if some recorded-fake
was never marked checked, i.e. you forgot to assert its calls.
Monkey Patching
You can temporarily change a variable value by using patch!
macro:
(f/patch! var-expr val)
(fc/patch! ctx var-expr val)
After patching original value can still be obtained using a function:
(f/original-val a-var)
(fc/original-val ctx a-var)
Also don't forget to unpatch the variable to recover its original value:
(f/unpatch! var-expr)
(fc/unpatch! ctx var-expr)
Or unpatch all the variables inside the context at once:
(f/unpatch-all!)
(fc/unpatch-all! ctx)
If you use with-fakes
then all variables will be unpatched
automatically on exiting the block, for instance:
(f/with-fakes
(f/patch! #'funcs/sum (f/fake [[1 2] "foo"
[3 4] "bar"]))
(is (= "foo" (funcs/sum 1 2)))
(is (= "bar" (funcs/sum 3 4))))
; patching is reverted on exiting with-fakes block
(is (= 3 (funcs/sum 1 2)))
Another example is combining patch
and recorded-fake
in order
to create a function spy which works exactly the same as the original function
and also records its calls:
(f/patch! #'funcs/sum (f/recorded-fake [f/any funcs/sum]))
Monkey patching is not thread-safe because it changes variable
in all threads
(underlying implementation uses
alter-var-root
/set!
).
Starting from Clojure 1.8, if direct linking is enabled:
- you have to add
^:redef
metadata key to functions which you patch; - you can't patch core functions (e.g.
println
).