莊周夢蝶

          生活、程序、未來
             :: 首頁 ::  ::  :: 聚合  :: 管理

          Clojure世界:單元測試

          Posted on 2012-02-15 19:39 dennis 閱讀(5070) 評論(0)  編輯  收藏 所屬分類: Clojure
              單元測試也是一個開發中最常見的需求,在Java里我們用JUnit或者TestNG,在clojure里也內置了單元測試的庫。標準庫的clojure.test,以及第三方框架midje。這里我將主要介紹clojure.test這個標準庫,midje是個更加強大的測試框架,廣告下,midje的介紹在第二次cn-clojure聚會上將有個Topic,我就不畫蛇添足了。通常來說,clojure.test足夠讓你對付日常的測試。

              首先看一個最簡單的例子,定義一個函數square來計算平方,然后我們測試這個函數:
          ;;引用clojure.test
          (ns example
            (:use [clojure.test :only [deftest 
          is run-tests]]))
          ;;定義函數
          (defn square [x]
            (
          * x x))
          ;;測試函數
          (deftest test
          -square
            (
          is (= 4 (square 2)))
            (
          is (= 9 (square -3))))
          ;;運行測試
          (run
          -tests 'example)

              執行輸出:

          Testing example

          Ran 
          1 tests containing 2 assertions.
          0 failures, 0 errors.

              這個小例子基本說明了clojure.test的主要功能。首先是斷言is,類似JUnit里的assertTrue,用來判斷form是否為true,它還可以接受一個額外的msg參數來描述斷言:
           (is (= 4 (square 2)) "a test")
              它還有兩種變形,專門用來判斷測試是否拋出異常:
           (is (thrown? RuntimeException (square "a")))
           (
          is (thrown-with-msg? RuntimeException #"java.lang.String cannot be cast to java.lang.Number"  (square "a")))
              上面的例子故意求"a"的平方,這會拋出一個java.lang.ClassCastException,一個運行時異常,并且異常信息為java.lang.String cannot be cast to java.lang.Number。我們可以通過上面的方式來測試這種意外情況。clojure.test還提供了另一個斷言are,用來判斷多個form:
           (testing "test zero or one"
              (are
               (
          = 0 (square 0))
               (
          = 1 (square 1))))
              are接受多個form并判斷是否正確。這里還用了testing這個宏來添加一段字符串來描述測試的內容。

              其次,我們用deftest宏定義了一個測試用例,deftest定義的測試用例也可以組合起來:
             (deftest addition
               (
          is (= 4 (+ 2 2)))
               (
          is (= 7 (+ 3 4))))
             (deftest subtraction
               (
          is (= 1 (- 4 3)))
               (
          is (= 3 (- 7 4))))
             (deftest arithmetic
               (addition)
               (subtraction))

              但是組合后的tests運行就不能簡單地傳入一個ns,而需要定義一個test-ns-hook指定要跑的測試用例,否則組合的用例如上面的addition和subtraction會運行兩次。我們馬上談到。

              定義完用例后是運行測試,運行測試使用run-tests,可以指定要跑測試的ns,run-tests接受可變參數個的ns。剛才提到,組合tests的時候會有重復運行的問題,要防止重復運行,可以定義一個test-ns-hook的函數:
          (defn test-ns-hook []
            (test
          -square)
            (arithmetic))
              這樣run-tests就會調用test-ns-hook按照給定的順序執行指定的用例,避免了重復執行。

              在你的測試代碼里明確調用run-tests執行測試是一種方式,不過我們在開發中更經常使用的是lein來管理project,lein會將src和test分開,將你的測試代碼組織在專門的test目錄,類似使用maven的時候我們將main和test分開一樣。這時候就可以簡單地調用:
                  lein test
          命令來執行單元測試,而不需要明確地在測試代碼里調用run-tests并指定ns。更實用的使用例子可以看一些開源項目的組織。

              單元測試里做mock也是比較常見的需求,在clojure里做mock很容易,原來clojure.contrib有個mock庫,基本的原理都是利用binding來動態改變被mock對象的功能,但是在clojure 1.3里,binding只能改變標注為dynamic的變量,并且clojure.contrib被廢棄,部分被合并到core里面,Allen Rohner編譯了一個可以用于clojure 1.3的clojure.contrib,不過需要你自己install到本地倉庫,具體看這里。不過clojure.contrib.mock哪怕使用1.2的編譯版本其實也是可以的。

              clojure.contrib最重要的是expect宏,它類似EasyMock里的expect方法,看一個例子:
          (use [clojure.contrib.mock :only [times returns has-args expect]])

          (deftest test
          -square2
            (expect [square (has
          -args [number?] (times 2 (returns 9)))]
                    (
          is (= 9 (square 4)))))

              has-args用來檢測square的參數是不是number,times用來指定預期調用的次數,而returns用來返回mock值,是不是很像EasyMock?因為我們這個測試只調用了square一次,所以這個用例將失敗:
          Testing example
          "Unexpected invocation count. Function name: square expected: 2 actual: 1"
             這個例子要在Clojure 1.3里運行,需要將square定義成dynamic:
          (defn ^:dynamic square [x]
            (
          * x x))
             否則會告訴你沒辦法綁定square:
          actual: java.lang.IllegalStateException: Can't dynamically bind non-dynamic var: example/square


              額外提下,還有個輕量級的測試框架expections可以看一下,類似Ruby Facker的facker庫提供一些常見的模擬數據,如名稱地址等。
             
               轉載請注明出處:http://www.aygfsteel.com/killme2008/archive/2012/02/15/370040.html

             
          主站蜘蛛池模板: 绵阳市| 潼关县| 赤城县| 赤水市| 盱眙县| 乐清市| 浦北县| 玛纳斯县| 连云港市| 久治县| 元氏县| 舒兰市| 古交市| 乌鲁木齐县| 砀山县| 萝北县| 刚察县| 九寨沟县| 恩施市| 嵊州市| 普兰县| 六盘水市| 许昌县| 南京市| 海原县| 樟树市| 武山县| 余姚市| 大理市| 新民市| 新宾| 汤原县| 宁远县| 陆河县| 津南区| 天津市| 读书| 临汾市| 建昌县| 尤溪县| 南平市|