返回 Skill 列表
extension
分类: 开发与工程无需 API Key

lang-clojure-dev

基础的Clojure模式涵盖了函数式编程、REPL驱动开发、不可变数据结构和惯用代码。在编写Clojure代码、处理序列和惰性求值、理解宏或需要函数式编程模式指导时使用。这是Clojure开发的入门点。

person作者: jakexiaohubgithub

Clojure Fundamentals

Foundational Clojure patterns and core language features. This skill serves as a reference for idiomatic Clojure development and functional programming practices.

Overview

This skill covers:

  • Core syntax (functions, data structures, special forms)
  • Functional programming patterns
  • REPL-driven development workflow
  • Immutable data structures and persistent collections
  • Sequence operations and lazy evaluation
  • Destructuring and pattern matching
  • Macros and metaprogramming basics
  • Concurrency primitives (atoms, refs, agents)
  • Java interop fundamentals
  • Serialization (EDN, JSON, Transit, clojure.spec)
  • Build/dependencies (Leiningen, deps.edn, tools.build)
  • Testing (clojure.test, test.check, Midje)

This skill does NOT cover:

  • ClojureScript and web development - see lang-clojurescript-dev
  • Specific frameworks (Ring, Compojure, etc.) - see framework-specific skills
  • Advanced spec patterns and generative testing - see dedicated testing skills

Quick Reference

| Task | Pattern | |------|---------| | Define function | (defn name [args] body) | | Anonymous function | (fn [x] (* x x)) or #(* % %) | | Create vector | [1 2 3] or (vector 1 2 3) | | Create map | {:key "value"} or (hash-map :key "value") | | Create set | #{1 2 3} or (hash-set 1 2 3) | | Thread-first | (-> x (f) (g)) = (g (f x)) | | Thread-last | (->> coll (map f) (filter pred)) | | Conditional | (if test then else) | | Pattern match | (case x 1 :one 2 :two :default) | | List comprehension | (for [x coll] (transform x)) |


Core Data Structures

Lists

;; Lists - linked lists, evaluated as function calls
'(1 2 3)          ; Quoted list (not evaluated)
(list 1 2 3)      ; Create list
(cons 0 '(1 2 3)) ; => (0 1 2 3)
(first '(1 2 3))  ; => 1
(rest '(1 2 3))   ; => (2 3)
(nth '(1 2 3) 1)  ; => 2

Vectors

;; Vectors - indexed access, grow at the end
[1 2 3]           ; Vector literal
(vector 1 2 3)    ; Create vector
(conj [1 2] 3)    ; => [1 2 3] (add to end)
(get [1 2 3] 1)   ; => 2
([1 2 3] 1)       ; => 2 (vectors are functions)
(assoc [1 2 3] 1 42) ; => [1 42 3]
(subvec [1 2 3 4 5] 1 4) ; => [2 3 4]

Maps

;; Maps - key-value pairs
{:name "Alice" :age 30}  ; Map literal
(hash-map :a 1 :b 2)     ; Create map
(get {:a 1} :a)          ; => 1
({:a 1} :a)              ; => 1 (maps are functions)
(:a {:a 1})              ; => 1 (keywords are functions)
(assoc {:a 1} :b 2)      ; => {:a 1 :b 2}
(dissoc {:a 1 :b 2} :b)  ; => {:a 1}
(update {:a 1} :a inc)   ; => {:a 2}
(merge {:a 1} {:b 2})    ; => {:a 1 :b 2}

;; Nested updates
(assoc-in {:user {:name "Alice"}} [:user :age] 30)
;; => {:user {:name "Alice" :age 30}}

(update-in {:user {:count 0}} [:user :count] inc)
;; => {:user {:count 1}}

Sets

;; Sets - unique elements
#{1 2 3}              ; Set literal
(hash-set 1 2 3)      ; Create set
(conj #{1 2} 3)       ; => #{1 2 3}
(disj #{1 2 3} 2)     ; => #{1 3}
(contains? #{1 2 3} 2) ; => true
(#{1 2 3} 2)          ; => 2 (sets are functions)

;; Set operations
(clojure.set/union #{1 2} #{2 3})        ; => #{1 2 3}
(clojure.set/intersection #{1 2} #{2 3}) ; => #{2}
(clojure.set/difference #{1 2} #{2 3})   ; => #{1}

Functions

Defining Functions

;; Named function
(defn greet
  "Returns a greeting for the given name."
  [name]
  (str "Hello, " name "!"))

;; Multi-arity function
(defn greet
  ([] (greet "World"))
  ([name] (str "Hello, " name "!"))
  ([greeting name] (str greeting ", " name "!")))

;; Variadic function (variable arguments)
(defn sum [& numbers]
  (reduce + numbers))

(sum 1 2 3 4) ; => 10

;; Pre and post conditions
(defn divide [numerator denominator]
  {:pre [(not= denominator 0)]
   :post [(number? %)]}
  (/ numerator denominator))

Anonymous Functions

;; Full form
(fn [x] (* x x))

;; Short form
#(* % %)

;; Multiple arguments
#(+ %1 %2)

;; Using in higher-order functions
(map #(* % 2) [1 2 3]) ; => (2 4 6)
(filter #(> % 5) [3 7 2 8]) ; => (7 8)

Function Composition

;; comp - right to left composition
(def process (comp str inc))
(process 5) ; => "6"

;; partial - partial application
(def add5 (partial + 5))
(add5 10) ; => 15

;; complement - logical negation
(def not-empty? (complement empty?))
(not-empty? [1 2 3]) ; => true

Sequence Operations

Core Sequence Functions

;; map - transform each element
(map inc [1 2 3]) ; => (2 3 4)
(map + [1 2 3] [10 20 30]) ; => (11 22 33)

;; filter - keep matching elements
(filter even? [1 2 3 4]) ; => (2 4)

;; remove - inverse of filter
(remove even? [1 2 3 4]) ; => (1 3)

;; reduce - accumulate
(reduce + [1 2 3 4]) ; => 10
(reduce + 100 [1 2 3]) ; => 106 (with initial value)

;; take / drop
(take 3 [1 2 3 4 5]) ; => (1 2 3)
(drop 2 [1 2 3 4 5]) ; => (3 4 5)

;; take-while / drop-while
(take-while #(< % 5) [1 2 6 7 3]) ; => (1 2)
(drop-while #(< % 5) [1 2 6 7 3]) ; => (6 7 3)

Lazy Sequences

;; Infinite sequences
(def naturals (iterate inc 0))
(take 5 naturals) ; => (0 1 2 3 4)

;; Lazy evaluation
(def evens (filter even? naturals))
(take 3 evens) ; => (0 2 4)

;; repeat
(take 3 (repeat "hi")) ; => ("hi" "hi" "hi")

;; cycle
(take 5 (cycle [1 2])) ; => (1 2 1 2 1)

;; range
(range 5)      ; => (0 1 2 3 4)
(range 2 7)    ; => (2 3 4 5 6)
(range 0 10 2) ; => (0 2 4 6 8)

List Comprehension

;; for - list comprehension
(for [x [1 2 3]]
  (* x x))
;; => (1 4 9)

;; Multiple bindings (cartesian product)
(for [x [1 2]
      y [3 4]]
  [x y])
;; => ([1 3] [1 4] [2 3] [2 4])

;; With :when (filter)
(for [x (range 10)
      :when (even? x)]
  x)
;; => (0 2 4 6 8)

;; With :let (local binding)
(for [x [1 2 3]
      :let [y (* x x)]]
  [x y])
;; => ([1 1] [2 4] [3 9])

Destructuring

Sequential Destructuring

;; Vector destructuring
(let [[a b c] [1 2 3]]
  (+ a b c)) ; => 6

;; Rest binding
(let [[first & rest] [1 2 3 4]]
  rest) ; => (2 3 4)

;; Named arguments + rest
(let [[a b & more :as all] [1 2 3 4 5]]
  {:a a :b b :more more :all all})
;; => {:a 1 :b 2 :more (3 4 5) :all [1 2 3 4 5]}

Associative Destructuring

;; Map destructuring
(let [{name :name age :age} {:name "Alice" :age 30}]
  (str name " is " age)) ; => "Alice is 30"

;; Shorthand with :keys
(let [{:keys [name age]} {:name "Alice" :age 30}]
  (str name " is " age))

;; With defaults
(let [{:keys [name age] :or {age 0}} {:name "Bob"}]
  age) ; => 0

;; Nested destructuring
(let [{{{city :city} :address} :user}
      {:user {:address {:city "NYC"}}}]
  city) ; => "NYC"

;; Function arguments
(defn greet-person [{:keys [name age]}]
  (str "Hello " name ", you are " age))

(greet-person {:name "Alice" :age 30})

Control Flow

Conditionals

;; if
(if (even? 4)
  "even"
  "odd") ; => "even"

;; if-not
(if-not (empty? [1 2 3])
  "has items"
  "empty") ; => "has items"

;; when (no else branch)
(when (pos? 5)
  (println "positive")
  "result") ; => "result"

;; when-not
(when-not (empty? [])
  "not empty") ; => nil

;; cond (multiple conditions)
(cond
  (< x 0) "negative"
  (> x 0) "positive"
  :else "zero")

;; condp (compare with predicate)
(condp = x
  1 "one"
  2 "two"
  3 "three"
  "other")

;; case (compile-time dispatch)
(case x
  1 "one"
  2 "two"
  "default")

Nil Handling

;; if-let (bind only if truthy)
(if-let [result (get {:a 1} :a)]
  (str "found: " result)
  "not found") ; => "found: 1"

;; when-let
(when-let [x (seq [1 2 3])]
  (first x)) ; => 1

;; some-> (thread-first, stop on nil)
(some-> {:a {:b 1}}
        :a
        :b
        inc) ; => 2

;; some->> (thread-last, stop on nil)
(some->> [1 2 3]
         (map inc)
         (filter even?)
         first) ; => 2

;; or (return first truthy value)
(or nil false 0 "result") ; => 0

Threading Macros

;; -> (thread-first)
(-> 5
    (+ 3)
    (* 2)
    (- 1)) ; => 15
;; Equivalent to: (- (* (+ 5 3) 2) 1)

;; ->> (thread-last)
(->> [1 2 3 4 5]
     (map inc)
     (filter even?)
     (reduce +)) ; => 12

;; as-> (thread with named argument)
(as-> 0 $
  (inc $)
  (+ 3 $)
  (* 2 $)) ; => 8

;; cond-> (conditional threading)
(cond-> []
  true (conj 1)
  (even? 2) (conj 2)
  false (conj 3)) ; => [1 2]

;; cond->> (conditional thread-last)
(cond->> [1 2 3]
  true (map inc)
  false (filter even?)) ; => (2 3 4)

Namespaces and Requires

;; Define namespace
(ns myapp.core
  "Application core namespace."
  (:require [clojure.string :as str]
            [clojure.set :as set]
            [myapp.util :refer [helper]]
            [myapp.config :refer :all]))

;; Refer specific functions
(:require [clojure.string :refer [upper-case lower-case]])

;; Import Java classes
(:import [java.util Date Calendar]
         [java.io File])

;; Using functions from required namespaces
(str/upper-case "hello") ; => "HELLO"
(set/union #{1 2} #{2 3}) ; => #{1 2 3}

State Management

Atoms

;; Create atom
(def counter (atom 0))

;; Read value
@counter ; => 0

;; Update with swap!
(swap! counter inc) ; => 1
(swap! counter + 5) ; => 6

;; Set value with reset!
(reset! counter 0) ; => 0

;; Conditional update with compare-and-set!
(compare-and-set! counter 0 10) ; => true if current value is 0

Refs (Coordinated, Synchronous)

;; Create refs
(def account-a (ref 100))
(def account-b (ref 200))

;; Transaction with dosync
(dosync
  (alter account-a - 50)
  (alter account-b + 50))

;; Read consistent values
(dosync
  [@account-a @account-b]) ; => [50 250]

;; commute (optimistic update)
(dosync
  (commute account-a + 10))

Agents (Asynchronous)

;; Create agent
(def logger (agent []))

;; Send asynchronous update
(send logger conj "log entry 1")
(send logger conj "log entry 2")

;; Read value (may not be updated yet)
@logger

;; Wait for all actions to complete
(await logger)

;; send-off for blocking operations
(send-off logger
  (fn [logs]
    (Thread/sleep 1000)
    (conj logs "delayed entry")))

Macros

Using Macros

;; Macros expand at compile time
;; Quote to prevent evaluation
'(+ 1 2) ; => (+ 1 2) (unevaluated list)

;; Common built-in macros
(when condition
  (do-thing-1)
  (do-thing-2))

(defn name [args] body) ; defn is a macro

(-> x f g h) ; threading macros are macros

Defining Macros

;; Simple macro
(defmacro unless [condition & body]
  `(if (not ~condition)
     (do ~@body)))

(unless false
  (println "This runs")
  "result") ; => "result"

;; Macro with syntax quote
(defmacro debug [expr]
  `(let [result# ~expr]
     (println '~expr "=>" result#)
     result#))

(debug (+ 1 2))
;; Prints: (+ 1 2) => 3
;; Returns: 3

;; Auto-gensym with #
(defmacro with-logging [& body]
  `(let [start# (System/currentTimeMillis)]
     (let [result# (do ~@body)]
       (println "Took" (- (System/currentTimeMillis) start#) "ms")
       result#)))

Java Interop

;; Create Java object
(new java.util.Date)
(java.util.Date.) ; Shorthand

;; Call instance method
(.toUpperCase "hello") ; => "HELLO"
(.substring "hello" 1 3) ; => "el"

;; Call static method
(Math/abs -5) ; => 5
(System/getProperty "java.version")

;; Access field
(.length "hello") ; => 5

;; Chain calls with ..
(.. "hello"
    (toUpperCase)
    (substring 0 3)) ; => "HEL"

;; doto (call multiple methods on same object)
(doto (java.util.HashMap.)
  (.put "a" 1)
  (.put "b" 2))

REPL-Driven Development

Workflow

;; 1. Start REPL
;; lein repl or clj

;; 2. Load namespace
(require '[myapp.core :as core] :reload)

;; 3. Test function interactively
(core/my-function "test input")

;; 4. Inspect data
(pprint complex-data-structure)

;; 5. Check documentation
(doc map)
(source map)

;; 6. Find functions
(apropos "str")
(find-doc "sequence")

;; 7. Examine namespace
(dir clojure.string)
(ns-publics 'clojure.string)

Common REPL Utilities

;; Pretty print
(require '[clojure.pprint :refer [pprint]])
(pprint {:a 1 :b 2 :c {:d 3}})

;; Inspect Java classes
(require '[clojure.reflect :as r])
(r/reflect String)

;; Test assertions
(assert (= 4 (+ 2 2)))

;; Time execution
(time (reduce + (range 1000000)))

Common Idioms

Pipeline Processing

;; Transform data through pipeline
(->> data
     (map parse-record)
     (filter valid?)
     (map transform)
     (group-by :category)
     (into (sorted-map)))

Error Handling

;; try/catch
(try
  (/ 1 0)
  (catch ArithmeticException e
    (println "Error:" (.getMessage e))
    nil)
  (finally
    (println "Cleanup")))

;; With custom exceptions
(try
  (when (invalid? data)
    (throw (ex-info "Invalid data"
                    {:data data :reason :validation})))
  (process data)
  (catch clojure.lang.ExceptionInfo e
    (let [{:keys [data reason]} (ex-data e)]
      (log/error "Failed:" reason))))

Memoization

;; Cache function results
(def fib
  (memoize
    (fn [n]
      (if (<= n 1)
        n
        (+ (fib (- n 1))
           (fib (- n 2)))))))

(fib 40) ; Fast after first call

Transducers

;; Composable algorithmic transformations
(def xf
  (comp
    (map inc)
    (filter even?)
    (take 5)))

;; Apply to different contexts
(sequence xf (range)) ; => (2 4 6 8 10)
(into [] xf (range))  ; => [2 4 6 8 10]
(transduce xf + (range)) ; => 30

Troubleshooting

Nil Pointer Exceptions

Problem: NullPointerException when calling methods

(.toUpperCase nil) ; NullPointerException

Fix: Use nil-safe operations

(some-> nil .toUpperCase) ; => nil
(when-let [s "hello"]
  (.toUpperCase s))

Stack Overflow

Problem: Recursion without tail call optimization

(defn sum [n]
  (if (zero? n)
    0
    (+ n (sum (dec n))))) ; Not tail recursive

(sum 10000) ; StackOverflowError

Fix: Use recur for tail recursion

(defn sum [n]
  (loop [n n acc 0]
    (if (zero? n)
      acc
      (recur (dec n) (+ acc n)))))

;; Or use reduce
(defn sum [n]
  (reduce + (range (inc n))))

Lazy Sequence Realization

Problem: Unexpected performance due to lazy evaluation

;; This realizes the entire sequence multiple times
(let [nums (map expensive-fn (range 1000))]
  (+ (count nums) (first nums) (last nums)))

Fix: Force realization once with doall or vec

(let [nums (vec (map expensive-fn (range 1000)))]
  (+ (count nums) (first nums) (last nums)))

Keyword vs String Keys

Problem: Map lookup returns nil

(get {"name" "Alice"} :name) ; => nil

Fix: Use consistent key types

(get {:name "Alice"} :name) ; => "Alice"
;; Or convert
(keyword "name") ; => :name

Serialization

Clojure provides multiple serialization options, from the native EDN format to JSON and Transit for interoperability. Validation is handled through clojure.spec and schema libraries.

EDN (Extensible Data Notation)

;; EDN is Clojure's native data format
(require '[clojure.edn :as edn])

;; Read EDN string
(edn/read-string "{:name \"Alice\" :age 30}")
;; => {:name "Alice" :age 30}

;; Read with custom readers
(edn/read-string {:readers {'inst #(java.time.Instant/parse %)}}
                 "#inst \"2024-01-15T10:30:00Z\"")

;; Write EDN
(pr-str {:name "Alice" :age 30})
;; => "{:name \"Alice\", :age 30}"

;; Read from file
(edn/read-string (slurp "config.edn"))

JSON with Cheshire

;; deps.edn: {:deps {cheshire/cheshire {:mvn/version "5.12.0"}}}
(require '[cheshire.core :as json])

;; Parse JSON
(json/parse-string "{\"name\": \"Alice\", \"age\": 30}" true)
;; => {:name "Alice" :age 30} (true = keywordize keys)

;; Generate JSON
(json/generate-string {:name "Alice" :age 30})
;; => "{\"name\":\"Alice\",\"age\":30}"

;; Pretty print
(json/generate-string {:name "Alice" :items [1 2 3]} {:pretty true})

;; Custom encoders
(json/generate-string
  {:timestamp (java.time.Instant/now)}
  {:encoders {java.time.Instant (fn [v gen] (.writeString gen (str v)))}})

;; Streaming for large files
(json/parse-stream (io/reader "large.json") true)

Transit (High-Performance)

;; deps.edn: {:deps {com.cognitect/transit-clj {:mvn/version "1.0.333"}}}
(require '[cognitect.transit :as transit])
(import '[java.io ByteArrayOutputStream ByteArrayInputStream])

;; Write Transit
(defn to-transit [data]
  (let [out (ByteArrayOutputStream.)]
    (transit/write (transit/writer out :json) data)
    (.toString out)))

(to-transit {:name "Alice" :keywords #{:a :b}})
;; Preserves Clojure types including keywords and sets

;; Read Transit
(defn from-transit [s]
  (transit/read (transit/reader (ByteArrayInputStream. (.getBytes s)) :json)))

;; Custom handlers for domain types
(defrecord User [id name])

(def write-handlers
  {User (transit/write-handler
          (constantly "user")
          (fn [u] [(:id u) (:name u)]))})

Validation with clojure.spec

(require '[clojure.spec.alpha :as s])

;; Define specs
(s/def ::name (s/and string? #(< 0 (count %) 100)))
(s/def ::email (s/and string? #(re-matches #".+@.+\..+" %)))
(s/def ::age (s/and int? #(< 0 % 150)))
(s/def ::user (s/keys :req-un [::name ::email]
                      :opt-un [::age]))

;; Validate
(s/valid? ::user {:name "Alice" :email "alice@example.com"})
;; => true

;; Explain failures
(s/explain ::user {:name "" :email "invalid"})
;; Prints detailed validation errors

;; Conform (parse + validate)
(s/def ::id-or-name (s/or :id int? :name string?))
(s/conform ::id-or-name 42) ; => [:id 42]

;; Validate function arguments
(defn create-user [{:keys [name email] :as user}]
  {:pre [(s/valid? ::user user)]}
  (assoc user :id (java.util.UUID/randomUUID)))

;; Generate test data
(require '[clojure.spec.gen.alpha :as gen])
(gen/sample (s/gen ::name) 5)

See Also

  • patterns-serialization-dev - Cross-language serialization patterns

Build and Dependencies

Clojure has two primary build ecosystems: Leiningen (the traditional choice) and tools.deps/CLI (the modern Clojure CLI).

Leiningen (project.clj)

;; project.clj
(defproject myapp "0.1.0-SNAPSHOT"
  :description "My Clojure application"
  :url "https://github.com/user/myapp"
  :license {:name "EPL-2.0"}

  ;; Dependencies
  :dependencies [[org.clojure/clojure "1.11.1"]
                 [cheshire "5.12.0"]
                 [ring/ring-core "1.10.0"]]

  ;; Development dependencies
  :profiles {:dev {:dependencies [[midje "1.10.9"]]
                   :plugins [[lein-midje "3.2.1"]]}}

  ;; Entry point
  :main myapp.core
  :aot [myapp.core]

  ;; Resources
  :resource-paths ["resources"]
  :source-paths ["src"]
  :test-paths ["test"]

  ;; REPL configuration
  :repl-options {:init-ns myapp.core})
# Common Leiningen commands
lein new app myapp          # Create new project
lein deps                   # Download dependencies
lein repl                   # Start REPL
lein run                    # Run main function
lein test                   # Run tests
lein uberjar                # Build standalone JAR
lein install                # Install to local Maven repo
lein deploy clojars         # Publish to Clojars

tools.deps (deps.edn)

;; deps.edn
{:paths ["src" "resources"]

 :deps {org.clojure/clojure {:mvn/version "1.11.1"}
        cheshire/cheshire {:mvn/version "5.12.0"}
        ring/ring-core {:mvn/version "1.10.0"}}

 :aliases
 {:dev {:extra-paths ["dev"]
        :extra-deps {nrepl/nrepl {:mvn/version "1.0.0"}}}

  :test {:extra-paths ["test"]
         :extra-deps {io.github.cognitect-labs/test-runner
                      {:git/tag "v0.5.1" :git/sha "dfb30dd"}}}

  :build {:deps {io.github.clojure/tools.build {:mvn/version "0.9.4"}}
          :ns-default build}

  :outdated {:deps {com.github.liquidz/antq {:mvn/version "2.5.1109"}}
             :main-opts ["-m" "antq.core"]}}}
# Common CLI commands
clj                         # Start REPL
clj -X:test                 # Run tests
clj -M:dev -m myapp.core    # Run with alias
clj -A:dev:test             # Combine aliases
clj -Sdeps '{:deps {...}}'  # Add deps inline
clj -T:build uber           # Build uberjar

Dependency Sources

;; Maven (Clojars, Maven Central)
{:deps {ring/ring-core {:mvn/version "1.10.0"}}}

;; Git dependency
{:deps {io.github.user/lib {:git/tag "v1.0.0" :git/sha "abc123"}}}

;; Local project
{:deps {mylib {:local/root "../mylib"}}}

;; Git with specific path
{:deps {lib {:git/url "https://github.com/user/monorepo"
             :git/sha "abc123"
             :deps/root "libs/mylib"}}}

Build Script (tools.build)

;; build.clj
(ns build
  (:require [clojure.tools.build.api :as b]))

(def lib 'myapp/myapp)
(def version "0.1.0")
(def class-dir "target/classes")
(def basis (b/create-basis {:project "deps.edn"}))
(def jar-file (format "target/%s-%s.jar" (name lib) version))
(def uber-file (format "target/%s-%s-standalone.jar" (name lib) version))

(defn clean [_]
  (b/delete {:path "target"}))

(defn jar [_]
  (b/write-pom {:class-dir class-dir :lib lib :version version :basis basis})
  (b/copy-dir {:src-dirs ["src" "resources"] :target-dir class-dir})
  (b/jar {:class-dir class-dir :jar-file jar-file}))

(defn uber [_]
  (clean nil)
  (b/copy-dir {:src-dirs ["src" "resources"] :target-dir class-dir})
  (b/compile-clj {:basis basis :src-dirs ["src"] :class-dir class-dir})
  (b/uber {:class-dir class-dir :uber-file uber-file :basis basis
           :main 'myapp.core}))

Publishing to Clojars

;; deps.edn alias for deployment
{:aliases
 {:deploy {:deps {slipset/deps-deploy {:mvn/version "0.2.1"}}
           :exec-fn deps-deploy.deps-deploy/deploy
           :exec-args {:installer :remote :artifact "target/myapp.jar"}}}}

;; Or in project.clj
{:deploy-repositories [["clojars" {:url "https://repo.clojars.org"
                                   :username :env/CLOJARS_USERNAME
                                   :password :env/CLOJARS_TOKEN}]]}

Testing

Clojure's testing ecosystem includes the built-in clojure.test, property-based testing with test.check, and BDD-style testing with Midje.

clojure.test (Built-in)

(ns myapp.core-test
  (:require [clojure.test :refer [deftest testing is are]]
            [myapp.core :as core]))

;; Basic test
(deftest add-test
  (is (= 4 (core/add 2 2)))
  (is (= 0 (core/add -1 1))))

;; Grouped assertions with testing
(deftest user-validation-test
  (testing "valid users"
    (is (core/valid-user? {:name "Alice" :email "a@b.com"}))
    (is (core/valid-user? {:name "Bob" :email "b@c.org"})))

  (testing "invalid users"
    (is (not (core/valid-user? {:name "" :email "a@b.com"})))
    (is (not (core/valid-user? {:name "Alice" :email "invalid"})))))

;; Table-driven tests with are
(deftest arithmetic-test
  (are [x y expected] (= expected (core/add x y))
    1 1 2
    2 3 5
    -1 1 0
    0 0 0))

;; Testing exceptions
(deftest divide-test
  (is (thrown? ArithmeticException (core/divide 1 0)))
  (is (thrown-with-msg? Exception #"cannot be zero"
        (core/safe-divide 1 0))))

;; Fixtures for setup/teardown
(use-fixtures :once
  (fn [f]
    (println "Starting test suite")
    (f)
    (println "Test suite complete")))

(use-fixtures :each
  (fn [f]
    (reset! core/db {})  ; Clean state
    (f)))

test.check (Property-Based)

;; deps.edn: {:deps {org.clojure/test.check {:mvn/version "1.1.1"}}}
(ns myapp.props-test
  (:require [clojure.test :refer [deftest is]]
            [clojure.test.check :as tc]
            [clojure.test.check.generators :as gen]
            [clojure.test.check.properties :as prop]
            [clojure.test.check.clojure-test :refer [defspec]]))

;; Property: reverse is its own inverse
(defspec reverse-involutive 100
  (prop/for-all [v (gen/vector gen/small-integer)]
    (= v (vec (reverse (reverse v))))))

;; Property: sorted output
(defspec sort-produces-sorted 100
  (prop/for-all [v (gen/vector gen/small-integer)]
    (let [sorted (sort v)]
      (every? (fn [[a b]] (<= a b))
              (partition 2 1 sorted)))))

;; Custom generator
(def user-gen
  (gen/hash-map
    :id gen/uuid
    :name (gen/not-empty gen/string-alphanumeric)
    :age (gen/choose 18 100)
    :email (gen/fmap #(str % "@example.com")
                     gen/string-alphanumeric)))

(defspec user-roundtrip 50
  (prop/for-all [user user-gen]
    (= user (core/parse-user (core/serialize-user user)))))

;; Manual property check
(tc/quick-check 100
  (prop/for-all [n gen/nat]
    (= n (core/identity-fn n))))

Midje (BDD-Style)

;; deps.edn: {:deps {midje/midje {:mvn/version "1.10.9"}}}
(ns myapp.core-test
  (:require [midje.sweet :refer :all]
            [myapp.core :as core]))

;; Facts with arrows
(fact "addition works correctly"
  (core/add 2 2) => 4
  (core/add -1 1) => 0)

;; Tabular facts
(tabular
  (fact "multiplication table"
    (core/multiply ?x ?y) => ?result)
  ?x ?y ?result
  1  1  1
  2  3  6
  0  5  0)

;; Checking predicates
(fact "string processing"
  (core/process "hello") => string?
  (core/process "hello") => #(> (count %) 0))

;; Mocking/stubbing
(fact "external API calls"
  (core/fetch-user 123) => {:id 123 :name "Alice"}
  (provided
    (core/http-get "https://api.example.com/users/123")
      => {:status 200 :body "{\"id\":123,\"name\":\"Alice\"}"}))

;; Checking exceptions
(fact "division by zero throws"
  (core/divide 1 0) => (throws ArithmeticException))

;; Prerequisites
(against-background
  [(core/get-config) => {:db-url "test://db"}]
  (fact "uses test config"
    (core/db-url) => "test://db"))

Running Tests

# Leiningen
lein test                   # Run all tests
lein test myapp.core-test   # Run specific namespace
lein test :only myapp.core-test/add-test  # Run single test
lein midje                  # Run Midje tests

# tools.deps with test-runner
clj -X:test                 # Run all tests
clj -X:test :nses '[myapp.core-test]'  # Specific namespace

# REPL
(require '[clojure.test :refer [run-tests]])
(run-tests 'myapp.core-test)

Test Organization

myapp/
├── src/
│   └── myapp/
│       └── core.clj
├── test/
│   └── myapp/
│       ├── core_test.clj       ; Unit tests
│       ├── integration_test.clj ; Integration tests
│       └── props_test.clj      ; Property tests
├── dev/
│   └── user.clj                ; REPL utilities
└── deps.edn

Cross-Cutting Patterns

For cross-language comparison and translation patterns, see:

  • patterns-concurrency-dev - Async/await, channels, threads
  • patterns-serialization-dev - JSON, validation, struct tags
  • patterns-metaprogramming-dev - Decorators, macros, annotations

References