[文摘] 單元測試是不夠的,你也需要靜態型別

動態型別 vs 靜態型別的爭論

前兩天在 Google Reader 上看到這兩篇文章,覺得還滿有趣的,就摘要一下。

話說 Dynamic Typing 和 Static Typing 的爭論一直都在,而在支持 Dynamic Typing 的論點當中,常常被提到的一點就是「靜態型別語言的編譯器不能抓出所有的錯誤,所你還是需要完整的測試,而當你有了夠完整的測試後,編譯器的型別檢查就是多餘的」這個論點。

像我這兩天正巧在翻 JavaScript: The Good Parts 這本書,作者在第一章為 JavaScript 進行平反的時候,也提到類似的論點:

But it turns out that strong typing does not eliminate the need for careful testing.

And I have found in my work that the sorts of errors that strong type check-ing finds are not the errors I worry about.

On the other hand, I find loose typing to be liberating. I don’t need to form complex class hierarchies. And I never have to cast or wrestle with the type system to get the behavior that I want.

我想關於「靜態型別不能抓出所有的錯誤」應該是不爭的事實了,畢竟編譯器只是幫你檢查型別是否符合,而不是替你檢查程式的邏輯,一個編譯過關的程式,還是可能會有無窮迴圈的 bug 出現。

單元測試可以取代型別檢查嗎?

然後因為「有了詳細的單元測試就不需要型別檢查」這個論點實在太常見到了,於是 evanfarrer 這名勇者就跑去做這個實驗來實際驗證這個論點是否成立,並當做自己的碩士論文了,最後他的結論是 Unit testing isn't enough. You need static typing too

他的測試方法是去找四個 Open Source,而且具有單元測試的 Python 專案,然後用工人智慧將這些專案從動態型別的 Python 轉寫成靜態型別的 Haskell。

之後他再來看是否有任何單元測試中沒被發現的錯誤,因為 Haskell 的靜態型別檢查而被抓到,以及有多少個 construct 是用到動態型別的特性而無法直接被轉成 Haskell 的。

然後實驗的結果如下:

  1. 有三個個專案有 type error 因為靜態型別檢查而被抓出來。
  2. 全部的專案都有 run time error 因為靜態型別檢查而被抓出來。
  3. 有兩個專案用到了 Python 的 struct.pack 和 struct.unpack 所以無法直接轉寫成 Haskell,不過其中一個專案裡因為都是 hard-coded,所以作者認為一樣可以用 hard-coded function 來代替,而另一個專案作者認為有 "Simple workaround"。

由於上面的原因,所以論文作者認為 Unit Testing 還是無法代替 static typing 的檢查。

抓到錯誤的分類

當然,如果只是數據和 bug 列表的話就沒那麼有趣了,所以 Rafael Ferreira 又針對那篇文章中被 static typing 抓到的錯誤進行分類,發現大部份被抓到的錯誤可以歸納為以下兩大類:

  1. 在一個變數可能為 null 時,卻假設他一定有值。
  2. 使用了不存在的變數/函數/類別名稱……等等。

針對第一項的話,用 Java 的角度來說,就是發生 NullPointerException 啦,明明是個 null,結果你以為他有值,接著程式就爆炸了。

這種錯誤其實在 Java 這類有 null 這東西的程式語言當中一樣會發生。

不過因為 Haskell 裡沒有 null,當你要表示 null 的時候,必需使用 Maybe 這個東西來操作,他類似於我們之前提到的 Scala 裡的 Option[T],由於這會造成強迫你一定要檢查他是不是空值,所以你不會假定他一定有合法值。

至於第二類的錯誤的話,正是 static typing 的強項,舉例來說,像下面的 Python 程式碼:

def test():
    print "Hello World"

def greeting(x):

    if x > 10:
        test()

    if x > 20:
        test()

    if x > 30:
        tes()

如果你的單元測試沒有測到丟給 greeting() 的是大於 30 的狀況的話,那上面這個 type error 就不會被抓出來,換句話說,除非你的 Unit Test 的 coverage 是 100%,否則你很難確定你的程式沒有這類的 type error 出現。

然而,抓出這類的 type error 則恰恰是 static typing 所善長的,畢竟他在編譯的時候就需要檢查所有的執行路徑,並確保你呼叫的函式一定是被定義過的,所以這類的 type error 自然逃不過編譯器的法眼。

結論

該篇論文的作者的結論就是固然 Static Typing 並不能取代 Unit Testing,但 Unit Testing 同樣也不行取代 Static Typing 的檢查,所以你還是需要 Static Typing 來當成另一道防線!

然後想當然爾,接著雙方陣營的人馬就開始在他那篇文章下戰起來了……XD

回響