Private Functions
Declaring Private Functions
Two ways, same result (both create a :private metadata flag):
;; 1. defn- macro (most common)
(defn- secret-sauce
"Adds 1, then doubles."
[x]
(* 2 (inc x)))
;; 2. ^:private metadata on regular defn
(defn ^:private secret-sauce [x]
(* 2 (inc x)))Both produce a Var with :private true in its metadata. Calling it from outside its namespace throws a compile-time warning but still works at runtime — Clojure’s “private” is a convention, not a hard wall.
;; In another namespace:
(my-ns/secret-sauce 5)
;; WARNING: var: my-ns/secret-sauce is not private
;; => 12 (still runs!)How to Test Private Functions
There’s a spectrum of approaches, from most-recommended to quickest:
1. 🥇 Test through the public API (preferred)
If a private function is hard to test directly, that’s often a signal it should be extracted into its own namespace (where it becomes optionally public), or that your public API needs more coverage.
;; Instead of testing -secret-sauce directly:
(deftest test-public-flow
(is (= 12 (my-public-fn 5)))) ;; exercises -secret-sauce internally2. 🥈 #' var-quote to bypass privacy
In your test namespace, use the var-quote reader macro #' to access the private Var directly:
(ns my-app.core-test
(:require [my-app.core :as sut]
[clojure.test :refer :all]))
(deftest test-secret-sauce
(is (= 12 (sut/#'secret-sauce 5))))The #' tells Clojure “grab the Var by its symbol, I don’t care about its privacy metadata.” Works at runtime, no warnings.
3. 🥉 Full alternate: ^:dynamic + binding
For functions you anticipate wanting to mock or test differently:
;; Instead of defn-, make it dynamic-public:
(defn ^:dynamic ^:private secret-sauce [x] ...)
;; In tests, override with binding:
(binding [secret-sauce (fn [_] :mock)]
(my-public-fn 5))This is more ceremony but useful for dependency injection patterns.
Why defn- exists at all if it’s not enforced
| Reason | Detail |
|---|---|
| Documentation | Marks internal implementation details for human readers |
| Tooling | IDEs and linters (clj-kondo) flag external usage |
| Namespace clarity | Keeps public API surface explicit — only functions without - are the contract |
| Compile-time safety | Warnings during AOT compilation or REPL loading catch accidental usage |
Summary
| Approach | When to use |
|---|---|
| Test through public API | 🥇 Always preferred — cleaner design |
#'var-quote | 🥈 In tests when you need direct coverage of internals |
binding with ^:dynamic | 🥉 When you need mocking, not just direct testing |
| Just make it public | Only if every other client also needs it — don’t leak internals for tests |
The #' trick is what you’ll see in most real Clojure codebases (core.test, contrib, etc.). It’s simple, visible, and doesn’t require changing your production code.