dependency injection with Clojure
寫 clojure 的時候,雖然套用了 REPL-driven development 的開發方式,已經相對可以讓大多數的函數很快地做過測試。但是,隨著要開發的專案愈來愈大,還是一樣需要用標準的寫法來寫單元測試 (unit test) 。有一個非正規的統計,如果是 Ruby on Rail 的專案,一般而言,90% 的函數都是有副作用的。然而, clojure 語言的專案,往往只有 40% 的函數帶有副作用。
即使是寫 clojure 語言,還是會遇到有 side effect 的函數,那比較好的寫法是怎麼樣呢?
我查了一下 stackoverflow 之後,很快就找到了一個很好用的函數 with-redefs
。 stackoverflow 上的答案大意如下: 由於 clojure 語言有 Dynamic binding 的特性,使用 with-redefs
就可以實現同樣的語意了。
我試了一下,還真的管用,範例如下:
(deftest platform-contact-test
(testing "platform-contact"
; use the DI technique to test the function platform-contact
(is (= 170
(with-redefs [get-platform-contact (fn [_] (slurp "./resources/contact_data.txt"))]
(count (platform-contact (temp-platform-all))))))))
在這個範例中,原本的 get-platform-contact
函數是一個有副作用的函數,它會被 platform-contact
函數呼叫。 get-platform-contact
函數會發出一個 http request ,並且傳回遠端 server 上的資料,所以如果沒有加以代換,單元測試就會非常慢。用了 with-redefs
之後,就可以輕易地將 get-platform-contact
代換成一個會傳回固定檔案資料的函數,如此就可以執行快速的單元測試了。
對於 clojure 這種先進的特性, stackoverflow 上有一句評論: Needing a framework for DI is really just compensating for a lack of sufficient features in the language itself.