Clojure Guide — Advanced Testing: Beyond Unit Tests
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