Browse Learn Clojure Foundations as a Java Developer

Run and Test a Clojure App

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?”

Runtime Commands

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"))))

Add a Focused Test

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.

Add a Small Test Runner

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.

Leiningen Equivalents

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.

Test the Boundary Separately

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.

Debugging Failed Runs

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

Key Takeaways

  • Running Clojure code means loading a namespace from the classpath and invoking a function.
  • Keep pure logic separate from -main so tests stay small.
  • clojure.test is built in and maps naturally to Java unit-testing habits.
  • Test paths are explicit in Clojure CLI projects.
  • A small runner can make test commands CI-friendly by returning a nonzero exit code.

Quiz: Run and Test

### What does `clojure -M -m hello-world.core Ada` do? - [x] Loads `hello-world.core`, calls `-main`, and passes `Ada` as an argument - [ ] Compiles every namespace into an uberjar - [ ] Runs every test namespace automatically - [ ] Starts Leiningen > **Explanation:** `-M -m` is the main-invocation path for a namespace with `-main`. ### Why is `greeting` easier to test than `-main`? - [x] It returns data without doing console I/O. - [ ] It is a macro. - [ ] It requires no namespace. - [ ] It modifies global state. > **Explanation:** Pure functions are easier to test because assertions can compare return values directly. ### What should a command-line test runner do when tests fail? - [x] Exit with a nonzero status. - [ ] Print success anyway. - [ ] Delete the `target` directory. - [ ] Change the Clojure version. > **Explanation:** CI and shell workflows depend on nonzero exit codes to detect failed test runs.
Revised on Saturday, May 23, 2026