Run a small Clojure app from the command line, add focused clojure.test coverage, and create repeatable commands that feel predictable to Java engineers.
Running and testing a Clojure application is mostly about controlling the classpath and keeping side effects at the edge. If a Java project asks “which class has main?”, a Clojure project asks “which namespace should be loaded, and which function should be called?”
For the first project from the previous page, deps.edn can expose a run command:
1{:paths ["src" "resources"]
2 :deps {org.clojure/clojure {:mvn/version "1.12.4"}}
3 :aliases
4 {:run {:main-opts ["-m" "hello-world.core"]}}}
Run it directly:
1clojure -M -m hello-world.core Ada
Or through the alias:
1clojure -M:run Ada
| Command part | Meaning |
|---|---|
clojure |
Launches the Clojure CLI |
-M |
Runs with main options |
-m hello-world.core |
Loads the namespace and calls -main |
Ada |
Argument passed to -main |
The matching namespace:
1(ns hello-world.core)
2
3(defn greeting [name]
4 (str "Hello, " name "!"))
5
6(defn -main
7 [& args]
8 (println (greeting (or (first args) "Clojure"))))
Create test/hello_world/core_test.clj:
1(ns hello-world.core-test
2 (:require [clojure.test :refer [deftest is testing]]
3 [hello-world.core :as core]))
4
5(deftest greeting-test
6 (testing "builds a greeting"
7 (is (= "Hello, Ada!" (core/greeting "Ada")))))
This test checks the pure function, not the console. That is the same instinct you would use in Java when you keep business logic separate from System.out or framework code.
For a tiny CLI-only project, you can add your own runner instead of introducing a third-party test runner immediately.
Create test/hello_world/test_runner.clj:
1(ns hello-world.test-runner
2 (:require [clojure.test :as test]
3 [hello-world.core-test]))
4
5(defn -main
6 [& _]
7 (let [{:keys [fail error]} (test/run-tests 'hello-world.core-test)]
8 (when (pos? (+ fail error))
9 (System/exit 1))))
Update deps.edn:
1{:paths ["src" "resources"]
2 :deps {org.clojure/clojure {:mvn/version "1.12.4"}}
3 :aliases
4 {:run {:main-opts ["-m" "hello-world.core"]}
5 :test {:extra-paths ["test"]
6 :main-opts ["-m" "hello-world.test-runner"]}}}
Run the tests:
1clojure -M:test
Expected output includes a summary like:
1Ran 1 tests containing 1 assertions.
20 failures, 0 errors.
If the project uses Leiningen, use its project tasks:
| Task | Clojure CLI | Leiningen |
|---|---|---|
| Run app | clojure -M:run Ada |
lein run Ada |
| Run tests | clojure -M:test |
lein test |
| Inspect classpath | clojure -Spath |
lein classpath |
| Start REPL | clj |
lein repl |
Do not treat these tools as competing languages. They are different ways to assemble a JVM classpath and invoke Clojure code.
Most tests should call pure functions. Boundary behavior can still be tested when needed:
1(deftest main-output-test
2 (testing "prints a greeting"
3 (is (= "Hello, Ada!\n"
4 (with-out-str
5 (core/-main "Ada"))))))
Use boundary tests sparingly. They are helpful for command-line behavior, but they are more brittle than tests over pure return values.
| Symptom | Likely cause | First check |
|---|---|---|
Could not locate ... on classpath |
Namespace path mismatch or missing path | Compare namespace to file path; run clojure -Spath |
No namespace ... found |
Typo in -m or alias |
Check :main-opts |
| Test namespace cannot load source namespace | src or test not on classpath |
Check :paths and :extra-paths |
| Command exits successfully despite failed tests | Runner does not call System/exit |
Return nonzero on failures/errors |
| Works in REPL but not from command line | REPL has extra state loaded | Restart and run from a clean shell |
-main so tests stay small.clojure.test is built in and maps naturally to Java unit-testing habits.