如何用单元测试检查Clojure代码的架构合规性?
Great question! Drawing inspiration from Building Evolutionary Architectures and Clean Architecture to enforce layer dependencies (e.g., ensuring controllers only reference use-cases and never entities) is a smart way to keep your codebase maintainable. While Clojure doesn't have a direct 1:1 equivalent to ArchUnit, there are several effective tools and approaches to achieve this:
1. Clj-Kondo with Custom Linting Rules
Clj-Kondo is a powerful static analyzer for Clojure that lets you define custom linting rules to catch architectural violations early. It's lightweight, fast, and integrates with most editors/CI pipelines.
Example Custom Rule for Your Boundary
Create or update your .clj-kondo/config.edn file to add a rule that blocks controllers namespaces from referencing entities:
{:lint-as {clojure.core/require clj-kondo.lint-as/defun} :hooks {:analyze-call {clojure.core/require my.namespace.dependency-checker/check-require}} :config {:allowed-dependencies {"com.controller.*" #{"com.use-cases.*"}}}}
Then implement the checker function in my.namespace.dependency-checker:
(ns my.namespace.dependency-checker (:require [clj-kondo.hooks-api :as api])) (defn check-require [{:keys [node]}] (let [current-ns (api/current-ns) required-nses (map (comp api/sexpr second) (:children node)) allowed (get-in (api/config) [:config :allowed-dependencies current-ns])] (when allowed (doseq [ns required-nses] (when-not (some #(re-matches (re-pattern %) ns) allowed) (api/reg-finding! {:message (str "Namespace " current-ns " is not allowed to reference " ns) :type :architectural-violation :node node}))))))
Run Clj-Kondo on your codebase, and it will flag any controllers namespace that imports entities directly.
2. Eastwood Static Analyzer
Eastwood is another popular static analysis tool for Clojure that supports custom checkers. You can build a custom checker to validate namespace dependencies.
Example Custom Checker
Define a checker function that scans namespaces and validates their dependencies:
(ns my.eastwood.checkers.dependency-enforcer) (defn check-dependencies [report-map] (let [controllers-nses (filter #(re-find #"^com\.controller\." %) (:namespaces report-map))] (doseq [ns controllers-nses] (let [required-nses (->> ns find-ns ns-requires keys (map str)) forbidden (filter #(re-find #"^com\.entities\." %) required-nses)] (doseq [f forbidden] (swap! report-map :add-finding {:type :architectural-dependency-violation :message (str "Controller namespace " ns " references forbidden entity namespace " f) :filename (-> ns find-ns meta :file) :line-number 1}))))))
Configure Eastwood to use this checker in your eastwood.clj config, and it will report violations during analysis.
3. Custom Unit Tests with Namespace Utilities
If you prefer a test-driven approach without third-party tools, you can write unit tests using Clojure's built-in namespace functions or the clojure.tools.namespace library to inspect dependencies.
Example Unit Test
(ns com.architecture-tests.dependency-checks (:require [clojure.test :refer :all] [clojure.tools.namespace.find :as ns-find] [clojure.tools.namespace.parse :as ns-parse])) (deftest controllers-should-not-reference-entities (let [controller-nses (ns-find/find-namespaces-in-dir (clojure.java.io/file "src/com/controller")) entity-ns-pattern #"^com\.entities\."] (doseq [ns controller-nses] (require ns) (let [required-nses (-> ns find-ns ns-requires keys (map str)) forbidden-refs (filter #(re-find entity-ns-pattern %) required-nses)] (is (empty? forbidden-refs) (str "Controller namespace " ns " has forbidden references to: " (clojure.string/join ", " forbidden-refs)))))))
Running this test will fail if any controllers namespace directly references an entities namespace, just like your example code.
All these approaches let you enforce the clean architecture boundaries you're aiming for, keeping your codebase aligned with the principles from those great books.
内容的提问来源于stack exchange,提问作者Michel Uncini

