Clojure Don’ts: Thread as

A brief addendum to my last post about Clojure’s threading macros.

As I was saying …

I said you could use as-> to work around the placement of an argument that doesn’t quite fit into a larger pattern of ->. My example was:

(-> results
    (as-> matches (filter winning-match? matches))
    (nth 3)
    (get "total_points"))

This is not a good example, because filter is a lazy sequence function that should more properly be used with ->>. And I warned explicitly against mixing -> and ->>.

Here’s a better example.

Say you have three functions, each taking a “context map” as their first argument and returning a similar map:

(defn one [context ...] ...)
(defn two [context ...] ...)
(defn three [context ...] ...)

Those functions can be chained together neatly with ->:

(-> context
    (one ...)
    (two ...)
    (three ...))

Now say you need to insert a call to another function that doesn’t quite follow the same pattern, maybe from a library whose source you do not control:

(defn irritating [... context ...] ...)

Here’s where the as-> macro really shows its purpose. You can slip in a call to irritating without having to change the structure of the ->.

(-> context
    (one ...)
    (two ...)
    (as-> ctx (irritating ... ctx ...))  ; OK
    (three ...))

That’s a good use case for as->, but I would tread cautiously even here. as-> subverts the usual pattern of -> and, as such, should be used only in exceptional situations where the alternative is worse.

In this example, I would much rather rewrite the irritating function to remove the inconsistency. If that’s not impossible, as in the third-party library case, and I had to use the function frequently, I would probably wrap it in my own function that has the arguments in an order consistent with ->.

The design of as->

In the days before version 1.5, Clojure users often clamored for something like as->. Many “utility” libraries provided their own version, usually looking something like this:

(defmacro -$> [form & more]
  (if (seq more)
    `(let [~'$ ~form]
       (-$> ~@more))

This is an anaphoric macro, because it “captures” the local symbol $ (the anaphor) and binds it to a value. It’s used like this:

(-$> {:a 1}
     (assoc $ :b 2)
     (+ (:a $) (:b $))
     (* $ 2))
;;=> 6

I never liked these macros, because they practically encourage violations of my expectation for -> that the “threaded” value stay the same “type” throughout the expression.

The as-> macro is different in a subtle but important way. Not only does it force you to give a real name to the “threaded” value, it places that name second in its parameters. To me, this clearly indicates that it is meant to be used in combination with ->.

(-> ...
    (as->   name ...)
    ;;    ^ the "threaded value" arrives here

Outside of ->, the arguments to as-> appear in the order value name rather than name value, making it unlike anything else in Clojure.

Don’t use as-> by itself

Therefore, I can confidently say one should never use as-> by itself, as in:

;; Bad
(as-> (initial-value) it
      (one it ...)
      (two it ...)
      (irritating ... it ...)
      (three it ...))

The placement of the initial arguments is “backwards,” and the named symbol doesn’t communicate enough about its meaning throughout the expression. This is only marginally better than -$>, and often abused in the same way.

As in my last post, the response is the same: Either refactor the code to use consistent parameter placement, or just use let.

Threading with Style

No, not multi-threading. I’m talking about Clojure’s threading macros, better known as “the arrows.”

The -> (“thread-first”) and ->> (“thread-last”) macros are small but significant innovations in Clojure’s Lisp-like syntax. They support a functional style of programming with return values and make composition intuitive. They answer the two chief complaints about Lisp syntax: too many parentheses and “unnatural” ordering.

There’s something delightful about expressing a complex procedure as a neat sequence of operations with values threaded through it:

(-> input

I love writing code like this, and I love reading it. It’s like a table of contents, a shining path guiding me to through rest of the code.

All too often, however, I see the threading macros used in places where they make the code less clear. Early in Clojure’s public existence, there was a fad for “point-free” programming, a kind of code golf without any local variables. This is a fun exercise, but it does not lead to readable code, and I’m relieved that the enthusiasm for this style has worn off.

What has remained is a tendency to use threading macros any place they can be made to fit. I think this is a mistake. Rather than making the code easier to read, it makes it harder, forcing the reader to mentally “unwind” the syntax to figure out what it does.

With that in mind, here are my recommendations for situations where you should use threading macros.


Thread-first -> is ideal for navigating large, nested structures. The shape even suggests direction. The functions calls should be short and simple, usually a mix of keywords, ordinals (first, nth), and lookups (get). For example:

(-> results :matches (nth 3) :scores (get "total_points"))

This works with Java objects too:

(-> results .getMatches (nth 3) .getScores (.getKey "total_points"))

Be aware, though, that if you mix Java methods and generic Clojure functions (like nth in the example above) you will lose type inference on the arguments and might introduce reflection warnings. For a (contrived) example:

(set! *warn-on-reflection* true)
(import (java.util Date))

(let [m {:now (Date.)}]
  (-> m :now .getTime))
;; Reflection warning ...
;; reference to field getTime can't be resolved.

You could add type-hints to eliminate the reflection:

(let [m {:now (Date.)}]
  ;; We cannot type-hint a keyword,
  ;; so wrap it in a list:
  (-> m ^Date (:now) .getTime))
;; No reflection warning

As even this small example shows, type hints interrupt the linear flow of ->, so they are better off in a let (see below).

If you have only Java calls and you want to save a few characters, you can use .. instead of ->, which lets you omit the leading dot on the method names:

(.. results getMatches (getItem 3) getScores (getKey "total_points"))

The .. macro is nice for navigating deep object hierarchies or “builder” patterns in Java APIs.

I don’t have strong opinions about whether you use .. or -> for interop: The .. macro clearly signals that everything which follows is a Java method, but the leading dots on the method names in -> do that just as well. (Why have both? The .. macro pre-dates the .method syntax for Java interop, so early versions of Clojure could not use Java methods in ->.)


The second use case for -> is performing a series of functional transformations on a single value:

(-> username
    (string/replace #"[^a-z]" "-"))

You might have noticed that most standard Clojure functions take the “primary thing” as their first argument. Arguments which control the behavior of the function come after. Maps are an excellent example:

(-> game-state
    (assoc :next-player :player2)
    (update :turn-counter inc)
    (update-in [:scores :player1] + 10)
    (update-in [:scores :player2] - 3))

In general, I expect these transformations to start and end with data of the same or similar “type.” String in, String out. Map in, map out. It’s easy to imagine an object “flowing” through the -> if it keeps the same “shape.”

Changing types mid-stream is sometimes necessary, but try to avoid too many different types in a single ->. The exception to this rule is when you have to walk through several intermediate “types” to get to the one you want.

This contrived example converts a java.time.LocalDate into a DayOfWeek, then an integer:

(let [date (LocalDate/of 2018 1 1)]
  (-> date

This is more like navigation through a type hierarchy, so the section above applies.


Whereas most Clojure functions take the “main thing” as their first argument, the Sequence API functions take the sequence as their last argument. There are precedents for this in other languages, particularly cons in older Lisps. Since most Clojure sequence functions are lazy, you can think of them as “wrapping” the sequence in a transformation, such as map or filter. The introduction of transducers carries this design even further.

I believe the ->> (“thread-last”) macro should be used only with sequence functions:

(->> data
     (map :players)
     (mapcat :scores)
     (filter #(< 100 %))

The last operation in ->> will often be something that collects the elements of the sequence into a singular result, such as reduce, into, or (for side effects) run!.

This follows the same rules as “Transformation,” above, keeping the sequence “shape” throughout.

I don’t like reading code that uses ->> for anything other than sequences, because there’s rarely any other group of operations that consistently place the “main thing” at the end of their arguments. Just because a few calls happen to fit that pattern doesn’t necessarily make it a good use of ->>.


Navigation, transformation, and sequences: That’s it for the “do’s.” The rest are all “don’ts.”

Don’t mix -> and ->>

It might be tempting to do something like this:

;; Bad
(-> data
    (->> (map :final-score)
         (reduce +)))

It even works. I often do things like this at the REPL, to explore large data structures. But I refactor before committing it to code. It’s just too much mental effort to read when the argument is flipping positions constantly.

Don’t thread arithmetic

;; Bad
(-> celsius
    (* 9)
    (/ 5)
    (+ 32))

Maybe someone finds this readable. As far as I’m concerned, it might as well be FORTH.

Don’t use anonymous functions to change argument position

Never do this:

(-> results
    (#(filter winning-match? %))   ; BAD
    (nth 3)
    (get "total_points"))

This is a clever trick that happens to work, but it’s syntactically confusing. Instead, use a let to pull out intermediate values:

(let [wins (filter winning-match? (:matches results))]
  (-> wins
      (nth 3)
      (get "total_points")))

The as-> macro is another way to work around this issue:

(-> results
    (as-> matches (filter winning-match? matches))   ; OK
    (nth 3)
    (get "total_points"))

I don’t dislike as->, but I don’t like it much either. as-> can save you from rearranging a lot of code, but I would caution against overusing it.

Don’t mix threading macros with other syntactic macros

Never do this:

;; BAD
(->> rdr
     (map parse-line)
     (with-open [rdr (io/reader file)])
     (def lines))

Can you work out what that does? It took me three tries just to write it.

The threading macros are syntactic transformations. Don’t mix them with other syntax-like macros such as with-open or def.


What to do if you have something that almost fits neatly into a -> or ->>, but breaks one of these rules?

99% of the time, the answer is simple: use let. That’s what it’s for. Local symbols are an opportunity to add semantic meaning to your code by giving names to values.

The other 1% of the time, you can add or modify functions to make the process fit more neatly into the threaded pattern. If this is an easy change to make and it makes the code significantly more readable, then go for it. But don’t do it just to save 1 line from what would otherwise be a let.

Context Maps

I started this article with this example:

(-> input

In one sense, this is just a variation on transformation. Each of the step-* functions takes a single map argument and returns the same map, possibly with some additional data assoc’d in. Mentally I call this the “context map.”

You could say it’s a simplified version of the Blackboard Pattern, with the context map serving as the “blackboard.”

I find this pattern useful when trying to make sense of long procedures that implement a lot of complex business logic. It shows up often in frameworks such as Liberator and Pedestal.

When you have code that produces a lot of intermediate results, and you’re not sure when you might need one of those results again, a context map is a good place to stash them. If you use namespace-qualified keywords in the map, then different namespaces can share the same context map without risk of clashing keys. (Coincidentally, Clashing Keys is the name of my new synth-pop band.)

All that said, there are some aspects of this pattern that have always made me slightly uncomfortable. First, it’s easy to make mistakes, such as misspelling a keyword, that manifest as NullPointerExceptions. clojure.spec may help with this, but we’re still learning how best to apply it.

Second, and more critically, the functions that operate on these context maps often have implicit ordering constraints that are not expressed directly in the code. To modify code using this pattern, you have to be aware of the dependencies: which function adds a key to the context, and which functions use that key. I don’t have a good solution to this right now. Again, maybe clojure.spec will help.


So there you have it. Like many of Clojure’s “cool” features — lazy seqs, varargs, optional dynamic scope, polymorphism — the threading macros are powerful tools that should not be overused. Just because something can be written as -> or ->> doesn’t mean it should. As with anything else, the question to ask yourself is: Does this make the intention of the code more or less clear?

How ’bout that start-up time?

How long does Clojure start-up really take? Let’s find out.

Get yourself a Clojure project. Download the dependencies and pre-generate the classpath:

lein deps
lein classpath > cp.txt

This lets us run “raw” Clojure, without any tooling. Assuming a Bash-like shell:

time java -cp "$(cat cp.txt)" clojure.main -e '(System/exit 0)'

Now add Leiningen:

time lein run -m clojure.main -e '(System/exit 0)'

Next, add the Leiningen REPL:

time lein repl <<< "(exit)"

If you’re a fan of Emacs and CIDER, start Emacs and paste this into a scratch buffer:

(require 'cider)

(defvar cider-jack-in-start-time nil)

(defun start-timing-cider-jack-in (&rest args)
  (setq cider-jack-in-start-time (current-time)))

(defun elapsed-time-cider-jack-in (&rest args)
  (when cider-jack-in-start-time
    (prog1 (format "%.3f seconds"
                    (time-since cider-jack-in-start-time)))
      (setq cider-jack-in-start-time nil))))

(add-function :before
              (symbol-function 'cider-jack-in)
(setq cider-connection-message-fn

Evaluate that Elisp code with M-x eval-buffer, then open up your project.clj and run cider-jack-in.

Run each of these examples a few times to warm up the circuits.

How long does it take? On an empty project with just Clojure 1.8, I get:

java -cp … clojure.main 0.8 seconds
lein run -m clojure.main 2.2 seconds
lein repl 4.2 seconds
cider-jack-in 11.5 seconds

Yes, Clojure start-up could be faster, but make sure you know where the time is really going.

My environment: Leiningen 2.7.1, Oracle JDK 1.8.0_92, OS X

Clojure Don’ts: Non-Polymorphism

Polymorphism is a powerful feature. The purpose of polymorphism is to provide a single, consistent interface to a caller. There may be multiple ways to carry out that behavior, but the caller doesn’t need to know that. When you call a polymorphic function, you remain blissfully ignorant of (and therefore decoupled from) which method will actually run.

Don’t use polymorphism where it doesn’t exist.

All too often, I see protocols or multimethods used in cases where the caller does know which method is going to be called; where it is completely, 100% unambiguous, at every call site, which method will run.

As a contrived example, say we have this protocol with two record implementations:

(defprotocol Blerg
  (blerg [this]))

(defrecord Foo []
  (blerg [this]
    ;; ... do Foo stuff ...

(defrecord Bar []
  (blerg [this]
    ;; ... do Bar stuff ...

Then, elsewhere in the code, we have some uses of that protocol:

(defn process-foo [x]
  ;; ...
  (blerg x)  ; I know x is always a Foo
  ;; ...
(defn process-bar [x]
  ;; ...
  (blerg x)  ; I know x is always a Bar
  ;; ...

If you know which method will be called, it’s easy to fall into the trap of depending on that specific behavior. Now you’ve broken the abstraction barrier the protocol was meant to provide.

(defn process-bar [x]
  ;; ...
  (blerg x)  ; I know x is always a Bar

  ;; ... do something that relies on
  ;;     Bar's blerg having been called ...

Code like this is already tightly coupled, which isn’t necessarily a problem. The problem is that the coupling is hidden behind the implied decoupling of a protocol. That’s going to lead to bugs sooner or later.

Instead, write ordinary functions with distinct names and let the caller use the appropriate one.

(defn blerg-foo [foo]
  ;; ... do foo stuff ...

(defn blerg-bar [bar]
  ;; ... do bar stuff ...

(defn process-foo [x]
  ;; ...
  (blerg-foo x)
  ;; ...

(defn process-bar [x]
  ;; ...
  (blerg-bar x)
  ;; ...

Remember the Liskov Substitution Principle: If you cannot substitute one implementation of a protocol for another, it’s not a good abstraction.

This post is part of my Clojure Do’s and Don’ts series.

How to ns

Quick link: Stuart’s ns Style Guide

Everyone has their own personal quirks when it comes to syntax. No matter how hard you try to lock it down with code review, IDEs, scripts, or check-in hooks, individual differences will emerge.

In Clojure the situation is generally pretty stable: most people follow the same general patterns, which are implemented fairly consistently across editors and IDEs.

With one exception: the ns macro at the top of every file.

The original implementation of the ns macro in Clojure was short, simple, and effective. It was also spectacularly over-generalized. ns will take almost any combination of symbols, keywords, vectors, and lists and find something to evaluate.

There’s a spec of sorts in the docstring, but of course nobody reads that.

The laxness of the ns implementation was a constant thorn in my side as I worked on tools.namespace. Now it’s causing more headaches as macro specs introduced in Clojure 1.9.0-alpha11 uncover a bevy of bad syntax in libraries.

I’ll admit to having my own syntactic quirks when it comes to ns, but I make an effort to be consistent. After years of collecting preferences, I finally decided to write it all down.

So now you can read Stuart’s Opinionated Style Guide for Clojure Namespace Declarations and link to it during your next syntactic flamewar.

End of the Free Lunch

I’m part of that awkward, in-between cohort, a little too young to fit in with Gen Xers but — although we grew up with computers like our younger siblings, the much-loathed Millennials — still old enough to recall life before the Internet. The Oregon Trail Generation still remembers, dimly, the screech of a dial-up modem on a phone line that went into the wall.

When I was a kid, my family subscribed to three daily newspapers. My favorite was the Washington Post, because it had the most comic strips.

We didn’t buy newspapers because we wanted to read the news — that came free from radio or television. Buying newspapers was just what you did if you wanted to know what was going on: movie showtimes, restaurant hours, job listings, or who was selling a boat trailer.

Sure, there were other ways to get that information. With the Yellow Pages (remember those?) and some patience (remember that?) you could find just about anything. But it would take hours to gather up all the information conveniently dropped on your doorstep every morning. So we bought newspapers. And because we bought newspapers, so did advertisers.

Nobody minded if a little journalism happened along the way. It gave the whole enterprise a kind of public-spirited sheen, as if you were fulfilling a civic duty by skimming the “World” headlines before turning with a sigh to “Arts & Leisure” (in my case, for the comics).

As an Oregon Trail adult, I have never subscribed to a newspaper. I don’t recall having bought a newspaper since about 2003. In that time, newspaper revenues dropped by half while Google grew 1,452%. You, as my generation used to say, do the math.

Whenever I read another article (on the web) bemoaning the decline of newspapers, the end of journalism, and the collapse of civilization — these are, of course, written by journalists — I wonder what business they thought newspapers were in. Nobody ever paid for news. News is a commodity. Being the sole gatekeeper for information, now, that’s a good business to be in.

Today Google is the gatekeeper. So we go to Google, so go the advertisers, so no more subsidized journalism. Instead, we get subsidized email, calendars, word processors, spreadsheets, maps, directions, translations, web browsers, operating systems, and self-driving cars. All we had to give up was a little privacy.

And it’s not as if journalism is going to disappear completely. It will just have to get used to surviving like all the other unprofitable civic institutions: on the philanthropic whims of rich people.

Apathy of the Commons

Eight years ago, I filed a bug on an open-source project.

HADOOP-3733 appeared to be a minor problem with special characters in URLs. I hadn’t bothered to examine the source code, but I assumed it would be an easy fix. Who knows, maybe it would even give some eager young programmer the opportunity to make their first contribution to open-source.

I moved on; I wasn’t using Hadoop day-to-day anymore. About once a year, though, I got a reminder email from JIRA when someone else stumbled across the bug and chimed in. Three patches were submitted, with a brief discussion around each, but the bug remained unresolved. A clumsy workaround was suggested.

Linus’s Law decrees that Given enough eyeballs, all bugs are shallow. But there’s a correlary: Given enough hands, all bugs are trivial. Which is not the same as easy.

The bug I reported clearly affected other people: It accumulated nine votes, making it the fourth-most-voted-on Hadoop ticket. And it seems like something easy to fix: just a simple character-escaping problem, a missed edge case. A beginning Java programmer should be able to fix it, right?

Perhaps that’s why no one wanted to fix it. HADOOP-3733 is not going to give anyone the opportunity to flex their algorithmic muscles or show off to their peers. It’s exactly the kind of tedious, persistent bug that programmers hate. It’s boring. And hey, there’s an easy workaround. Somebody else will fix it, right?

Eventually it was fixed. The final patch touched 12 files and added 724 lines: clearly non-trivial work requiring knowledge of Hadoop internals, a “deep” bug rather than a shallow one.

One day later, someone reported a second bug for the same issue with a different special character.

If there’s a lesson to draw from this, it’s that programming is not just hard, it’s often slow, tedious, and boring. It’s work. When programmers express a desire to contribute to open-source software, we think of grand designs, flashy new tools, and cheering crowds at conferences.

A reward system based on ego satisfaction and reputation optimizes for interesting, novel work. Everyone wants to be the master architect of the groundbreaking new framework in the hip new language. No one wants to dig through dozens of Java files for a years-old parsing bug.

But sometimes that’s the work that needs to be done.

* * *

Edit 2016-07-19: The author of the final patch, Steve Loughran, wrote up his analysis of the problem and its solution: Gardening the Commons. He deserves a lot of credit for being willing to take the (considerable) time needed to dig into the details of such an old bug and then work out a solution that addresses the root cause.

Fixtures as Caches

I am responsible — for better or for worse — for the library which eventually became clojure.test. It has remained largely the same since it was first added to the language distribution back in the pre-1.0 days. While there are many things about clojure.test which I would do differently now — dynamic binding, var metadata, side effects — it has held up remarkably well.

I consider fixtures to be one of the less-well-thought-out features of clojure.test. A clojure.test fixture is a function which wraps a test function, typically for the purpose of setting up and tearing down the environment in which the test should run. Because test functions do not take arguments, the only way for a fixture to pass state to the test function is through dynamic binding. A typical fixture looks like this:

 (ns fixtures-example
   (:require [clojure.test :as test :refer [deftest is]]))
 (def ^:dynamic *fix*)
 (defn my-fixture [test-fn]
   (println "Set up *fix*")
   (binding [*fix* 42]
   (println "Tear down *fix*"))
 (test/use-fixtures :each my-fixture)
 (deftest t1
   (println "Do test t1")
   (is (= *fix* 42)))
 (deftest t2
   (println "Do test t2")
   (is (= *fix* (* 7 6))))

There are two kinds of fixtures in clojure.test:

:each fixtures run once per test, for every test in the namespace.

:once fixtures run once per namespace, wrapped around all tests in that namespace.

I think the design of fixtures has a lot of problems. Firstly, attaching them to namespaces was a bad idea, since namespaces typically contain many different tests, only some of which actually need the fixture. This increases the likelihood of unintended coupling between fixtures and test code.

Secondly, :each fixtures are redundant. If you need to wrap every test in some piece of shared code, all you need to do is put the shared code in a function or macro and call it in the body of each test function. There’s a small amount of duplication, but you gain flexibility to add tests which do not use the same shared code.

(Another common complaint about fixtures is that they make it difficult to execute single tests in isolation, although the addition of test-vars in Clojure 1.6 ameliorated that problem.)

So :once fixtures are the only ones that matter. But if you want true isolation between your tests then they should not share any state at all. The only reason for sharing fixtures across tests is when the fixture does something expensive or time-consuming. Here again, namespaces are often the wrong level of granularity. If some resource is expensive to prepare, you may only want to pay the cost of preparing it once for all tests in your project, not once per namespace.

So the purpose of :once fixtures is to cache their initialized state in between tests. What if we were to use fixtures only for caching? It might look something like this:

 (ns caching-example
   (:require [clojure.test :refer [deftest is]]))
 (def ^:dynamic ^:private *fix* nil)
 (defn new-fix
   "Computes a new 'fix' value for tests."
   (println "Computing fixed value")
 (defn fix
   "Returns the current 'fix' value for
   tests, creating one if needed."
   (or *fix* (new-fix)))
 (defn fix-fixture
   "A fixture function to provide a reusable
   'fix' value for all tests in a namespace."
   (binding [*fix* (new-fix)]
 (clojure.test/use-fixtures :once fix-fixture)
 (deftest t1
   (is (= (fix) 42)))
 (deftest t2
   (is (= (fix) (* 7 6))))

This still avoids repeated computation of the fix value, but clearly shows exactly which tests use it. The :once fixture is just an optimization: You could remove it and the tests would still work, perhaps more slowly. Best of all, you can run the individual test functions in the REPL without any additional setup.

The same idea works even if the fixture requires tear-down after tests are finished:

 (ns resource-example
   (:require [clojure.test :refer [deftest is]]))
 (defn acquire-resource []
   (println "Acquiring resource")
 (defn release-resource [resource]
   (println "Releasing resource"))
 (def ^:dynamic ^:private *resource* nil)
 (defmacro with-resource
   "Acquires resource and binds it locally to
   symbol while executing body. Ensures resource
   is released after body completes. If called in
   a dynamic context in which *resource* is
   already bound, reuses the existing resource and
   does not release it."
   [symbol & body]
   `(let [~symbol (or *resource*
      (try ~@body
             (when-not *resource*
               (release-resource ~symbol))))))
 (defn resource-fixture
   "Fixture function to acquire a resource for all
   tests in a namespace."
   (with-resource r
     (binding [*resource* r]
 (clojure.test/use-fixtures :once resource-fixture)
 (deftest t1
   (with-resource r
     (is (keyword? r))))
 (deftest t2
   (with-resource r
     (is (= "the-resource" (name r)))))
 (deftest t3
   (with-resource r
     (is (nil? (namespace r)))))

Again, each of these tests can be run individually at the REPL with no extra ceremony. If you don’t want to keep paying the resource-setup cost in the REPL, you could temporarily redefine the *resource* var in its initialized state.

The key in both cases is that the “fixtures” are designed to nest without duplicating effort. Each test function specifies exactly what state or resources it needs, but only creates them if they do not already exist. Some of those resources may be shared among multiple tests, but that fact is hidden from the individual tests.

With this in mind, it becomes possible to share a resource across all tests in a project, not just within a namespace. All you need is an “entry point” which kicks off all the tests. clojure.test provides run-tests for specifying individual namespaces and run-all-tests to search for namespaces by regex. All you have to do is make sure your test namespaces are loaded, either via direct require or a utility such as tools.namespace. Then you can run a full test suite that only executes the expensive setup/teardown code once:

 (ns main-test
   (:require [clojure.test :as test]
 (defn -main [& _]
       ;;; ... more fixture wrappers ...
       (test/run-all-tests #"^my\.app\..+-test$"))))