2009年4月19日 星期日

最近用 Python + TDD 心得(與 Java 做對照)

最近又用 Python 寫了些程式,剛好和之前用 Java 進行 TDD 做個對照。

若不知道 TDD 的人,可以先參考這篇,TDD 的概念是依以下三個步驟寫程式 ( 順序相當重要 ):

  1. 先寫簡單的單元測試,並執行它。
  2. 用最簡單的方法實作需要的功能,讓程式能通過測試。
  3. 重構程式,並確保重構後的程式仍能通過測試。

實作部份不需多談,這裡先分別對步驟一「寫測試碼」和步驟三「重構」討論,再分享一般性的一點心得。

寫測試碼

Python 寫起測試碼比 Java 簡單許多,可以輕易地抽換所有既有物件,如 module、function、class、method。下面是一個簡單的抽換 method 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass(object):
    def hello(self, msg):
        print msg
 
def new_hello(self, msg):
    print msg + " (by replaced method)"
 
obj = MyClass()
obj.hello('Testing rocks!')  # Testing rocks!
mystub = stub.Stub()
mystub.replace(MyClass, 'hello', new_hello)
obj.hello('Testing rocks!')  # Testing rocks! (by replaced method)
mystub.restore(MyClass, 'hello')
obj.hello('Testing rocks!')  # Testing rocks!

註解表示該行輸出結果,其中 Stub 是我寫得一個簡單 class,只有十行多,可以替換和還原物件。可以從 MyClass.hello() 的輸出看出,stub.replace() 後 MyClass.hello() 被換為 new_hello(),stub.restore() 後則換了回來。完整的程式碼請看這裡

方便抽換物件有什麼好處呢?舉例來說,若 class A 有兩個 method B 和 C,其中 B 用到 C ( B 會視 C 傳回結果改變程式流程 )。為了方便測試 B 的行為,得先控制 C 的傳回結果。若是 Python 的話,照上面的例子透過 Stub ( setattr() ) 替換 A.C 即可。但在 Java 的情況,除非將 C 當作參數傳給 B,讓測試程式有機會傳「假的 C 」給 B,不然難以控制 B 內部行為。寫新程式時還有機會改設計,對於舊的程式,修改程式是個災難。用到別人的函式庫或沒程式碼就無解了。不論 Java 有何解法 ( 如採用 injection 的方式 ),解法愈麻煩,表示大眾愈不願意做,因此降低測試碼的品質,連帶影響 TDD 效果。

Mock library 部份,Java 有 EasyMock,Python 有 Mox,兩者用法差不多。後者是前者改來的,學一套兩邊都可以用。

重構

由於 Java 的特性 ( static type ),重構工具相當成熟,我慣用 Eclipse 的重構功能,離開它就不太想寫 Java。其中我最常用的功能是改名稱,包含 variable、method 、 class、 package 等。好的名稱是好程式碼的必要條件之一,好的名稱可以省去冗長的註解、縮短理解程式的時間,同時也有助於作者釐清邏輯,減少犯錯的機會。若發現名稱很難取,也許表示該 object(method / class / package)功能沒規範好,之後容易遇到問題。寫程式難免會犯錯,寫一段時間後才發現命名不夠精確,得回頭修改。若有好的重構工具,改起來快又沒風險,可以提高重構的意願。

反觀 Python 因為 dynamic type,程式碼本身提供的訊息不足(得執行後才清楚全貌),難以透過工具重構。我找了一下相關工具後沒看到滿意的。Eclipse 的 PyDev plugin 有提供重構,但試用後結果是錯的,改 module 名稱時沒動到檔名,執行後才會發現程式碼爛了。另外試了 Rope ,可以正確改名,可是速度有點慢,操作相當不直覺 ( 之後再來試看看 ropevim )。為了長遠發展考量,或許可以試看看自己弄個簡單版的改名工具或改 Rope,工具的功能可以少,但要快、容易操作且結果正確。

結語

只有在三個步驟都確實做到時,TDD 才能發揮應有的威力。依我目前一點點的實作心得,Python 容易寫測試碼、卻不方便重構;而 Java 正好相反。現在我大多有照 TDD 的流程寫程式,即使寫個一小時的小程式,也會用 TDD 。有時寫寫覺得卡卡的,才發現忘了先寫測試碼。

另外,測試碼的範圍抓得準 ( 別測太細,也別懶得測核心) ,效果才會好。我初用 TDD + Python 時就矯枉過正,寫太多測試變成 「over-testing」,省了 over-design 的負擔,卻多花時間寫不必要的測試碼、又增加日後維護的成本 。

養成 TDD 習慣的關鍵,在於寫測試碼的功力,像是如何準備 fixture ,如何改善函式介面以利測試。為了寫測試而改變函式介面並不是本末倒置,通常這會降低函式之間的關聯性,將功能明確切開,讓每個函式的輸入輸出都很乾淨(有簡單的輸入,才方便準備 fixture)。而乾淨的輸入輸出意味著函式更容易被組合使用。

沒有留言:

張貼留言