Verify migrated Clojure behavior against Java by capturing golden fixtures, comparing normalized outputs, handling nondeterminism, testing adapters, and documenting intentional differences.
Functional equivalence means the migrated Clojure path preserves the Java behavior that callers and users depend on. It does not mean the internal design looks the same. In a good migration, Java classes may become Clojure functions and data, but the externally visible behavior must still be defensible.
For Java engineers, the safest validation strategy is to compare behavior at stable boundaries: method input/output pairs, HTTP request/response pairs, events, reports, or database-visible outcomes.
Define the contract before arguing about implementation details.
| Contract area | What to compare |
|---|---|
| Inputs | Required fields, optional fields, null handling, enum values, dates, money, and collection ordering. |
| Outputs | Result values, errors, warnings, status codes, events, and persisted state. |
| Side effects | Writes, notifications, queue messages, metrics, and audit records. |
| Ordering | Sort order, tie-breaking, pagination, and deterministic output shape. |
| Failure behavior | Expected domain failures versus broken system conditions. |
If a behavior is intentionally changed, record it as a migration decision. Do not let it appear as an unexplained mismatch.
A golden fixture is a reviewed input/output example from the old Java path.
1(def over-limit-fixture
2 {:input {:customer/id "C-42"
3 :customer/credit-limit 1000M
4 :orders [{:order/id "O-1"
5 :order/amount 700M}
6 {:order/id "O-2"
7 :order/amount 450M}]}
8 :expected {:summary/customer-id "C-42"
9 :summary/open-balance 1150M
10 :summary/status :over-limit}})
The Clojure test should prove the new function preserves that behavior.
1(deftest preserves-over-limit-summary
2 (let [{:keys [input expected]} over-limit-fixture]
3 (is (= expected (summarize-account input)))))
Do not generate fixtures from the new code. Capture them from the Java behavior or from reviewed business examples.
During migration, it is often useful to run both implementations against the same input and compare normalized results.
1(defn equivalent-summary? [java-summary clojure-summary]
2 (= (select-keys java-summary [:summary/customer-id
3 :summary/open-balance
4 :summary/status])
5 (select-keys clojure-summary [:summary/customer-id
6 :summary/open-balance
7 :summary/status])))
Normalization is important because Java and Clojure may represent the same business result differently. Normalize deliberately. Do not normalize away meaningful differences such as rounding, status codes, or missing warnings.
| Difference | Treat as |
|---|---|
| Map key order differs | Usually irrelevant if the public format does not promise order. |
| Money rounding differs | A real mismatch unless the business rule changed intentionally. |
| Warning text differs | Review with users or downstream consumers. |
| Extra audit event appears | A side-effect mismatch that can affect operations. |
Java returns null and Clojure returns nil internally |
Fine inside the adapter if the public contract is preserved. |
Property-based testing can find edge cases that example tests miss, but properties must be grounded in the migration contract.
1(def positive-money-gen
2 (gen/fmap bigdec (gen/choose 0 100000)))
3
4(def summary-total-property
5 (prop/for-all [amounts (gen/vector positive-money-gen)]
6 (let [orders (map-indexed (fn [idx amount]
7 {:order/id (str "O-" idx)
8 :order/amount amount})
9 amounts)
10 summary (summarize-account {:customer/id "C-1"
11 :customer/credit-limit 999999M
12 :orders orders})]
13 (= (:summary/open-balance summary)
14 (reduce + 0M amounts)))))
Use generated checks for invariants such as totals, ordering rules, idempotence, and validation behavior. Do not use them to avoid writing business examples.
Many migration bugs live in conversion code, not in Clojure’s pure logic.
| Adapter case | Why it matters |
|---|---|
| Java nulls | Decide where null becomes nil, rejection, or default data. |
BigDecimal values |
Preserve precision, scale, and rounding expectations. |
| Enums | Map to keywords and back without losing unsupported cases. |
| Lists and lazy sequences | Realize results before returning to Java when a concrete collection is expected. |
| Exceptions | Preserve enough context for Java callers and logs. |
Keep adapter tests close to the boundary. Core Clojure tests should not need Java service graphs.
Use this before cutover:
Functional equivalence is not a one-time assertion. Keep a small equivalence suite after cutover so future refactors do not reopen migration bugs.