Skip to content

Clojure Guide — Advanced Testing: Beyond Unit Tests

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Clojure Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

Advanced testing in Clojure extends beyond simple unit tests to include property-based testing with test.check, generative testing, integration testing with Ring, and test-driven development workflows for functional programs.

What You'll Learn

  • Property-based testing with test.check
  • Generative testing with generators
  • Test fixtures and setup/teardown
  • Integration testing Ring applications
  • Custom test macros

Why It Matters

Clojure's functional nature makes it especially suited for property-based testing, where you verify invariants across random inputs. Durga Antivirus Pro uses property-based testing to validate rules against millions of file signatures.

Real-World Use

Financial systems use property-based testing to validate trading algorithms. Security tools use generative testing to find edge cases in Parsing logic.

flowchart LR
    A["Advanced Testing"] --> B["test.check"]
    B --> C["Generators"]
    C --> D["Properties"]
    D --> E["Integration"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Getting Started with test.check

(require '[clojure.test :refer :all])
(require '[clojure.test.check :as tc])
(require '[clojure.test.check.properties :as prop])
(require '[clojure.test.check.clojure-test :refer [defspec]])

(defspec idempotent-sort
  100  ; 100 random tests
  (prop/for-all [v (gen/vector gen/int)]
    (= (sort v) (sort (sort v)))))

Generators

(require '[clojure.test.check.generators :as gen])

;; Basic generators
gen/int           ; Integers
gen/string        ; Strings
gen/boolean       ; Booleans
gen/keyword       ; Keywords

;; Composite generators
(gen/vector gen/int 1 10)        ; Vectors of ints, 1-10 length
(gen/map gen/keyword gen/string) ; Random maps
(gen/tuple gen/int gen/string)   ; Tuples

;; Custom generators
(def email-gen
  (gen/fmap
    (fn [[user domain]]
      (str user "@" domain ".com"))
    (gen/tuple gen/string gen/string)))

Property-Based Testing

;; Property: reversing a string twice gives the original
(defspec reverse-twice
  100
  (prop/for-all [s gen/string]
    (= s (clojure.string/reverse (clojure.string/reverse s)))))

;; Property: sorting preserves length
(defspec sort-preserves-length
  100
  (prop/for-all [v (gen/vector gen/int)]
    (= (count v) (count (sort v)))))

;; Property: sorting makes elements non-decreasing
(defspec sort-orders
  100
  (prop/for-all [v (gen/vector gen/int)]
    (apply <= (sort v))))

Conditional Properties

;; Property that only applies to non-empty collections
(defspec max-in-list
  100
  (prop/for-all [v (gen/such-that not-empty (gen/vector gen/int))]
    (let [m (apply max v)]
      (and (contains? (set v) m)
           (every? #(<= % m) v)))))

Test Fixtures

(use-fixtures :each
  (fn [f]
    (println "Before test")
    (f)
    (println "After test")))

;; Database fixture
(defn db-fixture [f]
  (let [conn (d/connect "datomic:mem://test")]
    (d/transact conn schema)
    (binding [*db* (d/db conn)]
      (f))
    (.release conn)))

Integration Testing Ring

(require '[ring.mock.request :as mock])
(require '[clojure.test :refer :all])

(deftest test-api-endpoint
  (let [request (mock/request :get "/api/users")
        response (app request)]
    (is (= 200 (:status response)))
    (is (coll? (json/parse-string (:body response))))))

(deftest test-post-with-body
  (let [request (-> (mock/request :post "/api/users")
                    (mock/body "{\"name\":\"Alice\"}")
                    (mock/content-type "application/json"))
        response (app request)]
    (is (= 201 (:status response)))))

Testing with State

(defn test-atom-based-cache []
  (let [cache (atom {})]
    (testing "cache put and get"
      (swap! cache assoc :key "value")
      (is (= "value" (@cache :key))))
    (testing "cache miss"
      (is (nil? (@cache :missing))))))

Custom Test Macros

(defmacro deftest-http [name method path & body]
  `(deftest ~name
     (let [request# (mock/request ~method ~path)
           response# (app request#)]
       ~@body)))

(defmacro is-ok [response]
  `(is (= 200 (:status ~response))))

;; Usage
(deftest-http test-home :get "/"
  (is-ok response)
  (is (clojure.string/includes? (:body response) "Welcome")))

Common Mistakes

1. Not shrinking failures

test.check shrinks failing cases to minimal examples. Use :seed and :max-size options for reproducibility.

2. Testing implementation, not behavior

Test properties and invariants, not specific values. Property-based testing shines for behavioral verification.

3. Over-mocking

In functional Clojure, pure functions don't need mocks. Pass dependencies as arguments.

4. Slow test suites

Property-based tests run many iterations. Use fewer iterations (50-100) during development, more in CI.

5. Ignoring edge cases

Property-based testing finds edge cases automatically. Review shrunken failure cases to fix bugs.

Practice Questions

1. What is property-based testing? Instead of testing specific inputs and outputs, property-based testing generates random inputs and checks that invariants hold for all cases.

2. What is test.check? test.check is Clojure's property-based Testing Library inspired by Haskell's QuickCheck. It generates random test data and shrinks failures.

3. How does shrinking work? When a property fails, test.check searches for the smallest input that still fails, making debugging easier.

Challenge: Write property-based tests for a custom sorting function that verify it's idempotent, preserves elements, and produces sorted output.

FAQ

{{< faq question="Is property-based testing better than example-based?" >}} They serve different purposes. Property-based tests find edge cases automatically. Example-based tests document specific behaviors. Use both. {{< /faq >}}

{{< faq question="How many test.check iterations should I use?" >}} 50-100 for development, 1000 for CI, 10000 for critical code. Higher counts find more rare edge cases. {{< /faq >}}

{{< faq question="Can test.check generate domain-specific data?" >}} Yes. Custom generators create valid domain data like email addresses, phone numbers, or structured business objects. {{< /faq >}}

{{< faq question="How do I debug failing properties?" >}} test.check prints the smallest failing input and the seed used. Reproduce with (tc/quick-check 100 prop :seed seed). {{< /faq >}}

{{< faq question="Do I need test.check for all tests?" >}} No. Use property-based tests for functions with clear invariants and example-based tests for specific workflows and edge cases you know about. {{< /faq >}}

Mini Project

Write property-based tests for a data validation library:

(defspec valid-email-format
  100
  (prop/for-all [email email-gen]
    (let [pattern #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"]
      (boolean (re-matches pattern email)))))

(defspec validate-sort-invariant
  100
  (prop/for-all [v (gen/vector gen/int)]
    (let [sorted (sort v)]
      (and (= (count v) (count sorted))
           (apply <= sorted)
           (= (set v) (set sorted))))))

What's Next

Now that you understand advanced testing, explore Leiningen profiles for managing project configurations.

Topic Description Link
Clojure Lein Profiles Project configuration {{< ref "25-lein-profiles" >}}
Clojure nREPL Interactive development {{< ref "26-nrepl" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro