Skip to content
Private Functions

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 internally

2. 🥈 #' 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

ReasonDetail
DocumentationMarks internal implementation details for human readers
ToolingIDEs and linters (clj-kondo) flag external usage
Namespace clarityKeeps public API surface explicit — only functions without - are the contract
Compile-time safetyWarnings during AOT compilation or REPL loading catch accidental usage

Summary

ApproachWhen 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 publicOnly 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.