星期四, 6月 17, 2004

Test infected

Test infected, 這個是 Test 大師 Kent Beck 形容 "非寫 test,不然就沒辦法 coding" 的一種習慣。這個字該怎麼翻好呢? "被測試感染"、"感染到測試"、"後天性測試症候群"、"急性測試炎".... -_-;; 好像太離譜了~~ 還是等待大師來翻譯正名吧.

Anyway, 這個禮拜又玩了一下 test-first 理論。這次從兩個方向著手,第一個是 service oriented 的程式。service 嘛,因為只知道它要做什麼,怎麼個做法還不知道,所以自然是從上到下 (top-down) 的方式來開發,先從最外面的輪廓著手:


//[code snip#1]
public class DataService {
   public void sendData(String data) {;}
}

(note: test 比這個早寫完)
接下來開始工作讓這個 service 工作,這個 service 會送資料到某個檔案,所以就變成這樣:


//[code snip#2 心中閃過的念頭,假的]
public class FileDataService {
  public FileDataService(String fileName) {
    this.fileName = fileName ;
    //etc....
  }
  public void sendData(String data) {
    String newFormat = formatConversion(data);
    new FileWriter(fileName).write(.....)
    //etc
  }
}

其實這個 FileDataService 並沒有真的寫出來,因為先寫 test 就知道 ==> 有夠難測!為了要 assert 這個程式的結果,我還要去找個真的檔案來寫,寫完後再來個 FileReader,讀完了之後,再把這個檔案給 purge 掉,暈倒... 為了讓 test 好寫,就將 fileName 拿掉,改用 OutputStream 當成介面:

//[code snip#3 test寫完後的第二個版本]
public class DataService {
  public DataService(OutputStream out) {
    this.out = out ;
    //etc....
  }
  public void sendData(String data) {
    String newFormat = formatConversion(data);
    out.write(...);
    //etc
  }
}

呼呼,這下 output 的來源被丟出去了,改成從 constructor 進來,這就叫 constructor injection 吧?測試這個程式就簡單多啦,我可以 new ByteArrayOutputStream() 給它,然後就.... 嘿,這個留給看倌自己測試啦,這個 class 很好用地~~ (hint: toByteArray() )。

後來這個程式演變成:
//[code snip#4 第三個版本]
public class DataService {
  public DataService(OutputFactory factory) {
    this.factory = factory ;
    //etc....
  }
  public void sendData(String data) {
    String newFormat = formatConversion(data);
    OutputStream out = factory.createOutputStream();
    out.write(...);
    //etc
  }
}

用一個factory來建立新的 OutputStream,因為這樣這個 service 才能送資料到不同地方,而這個 DataService 在進行測試時,其實 OutputFactory 還沒寫好,只是使用一個 dummy 的 MockOutputFactory 來輔助測試,DataService 才不管要寫到哪裡去咧,這是 OutputFactory 的事。它專心做它該做的事 (data conversion, write data... etc) 。由上面這個例子可知 test-first 帶來了: 逼你去想好測試的方法,把不相干的東西盡量丟到外部去,唯有這樣 Object Isolation 才會好。

Ok, 現在進入第二個例子:bottom-up,就是由小到大,從細部做起:我想設計一個 TableModel,有 cell, 也有 row,然後可以定義 cell 的 format,而且還有一個特殊功能,可以合併 table。由小到大,先寫 cell 、再寫 row、最後寫 Table

//[code snip#5 由小到大]
public class DataCell {
  public DataCell(Double data, DecimalFormat format) {;}
}
public class DataRow {
  public DataRow(int initialSize) {;}
  public void addCell(DataCell cell);
  public void appendRow(DataRow rowToCombine);
}

上面是寫完的結果,步驟是這樣地:首先先寫 DataCell 的 test,再完成 DataCell 本身 (DataCell設計成 immutable) 。有了 DataCell 之後,就開始寫 DataRow 的 test,這個 test 就好寫了,因為已經有"測試完整"的 DataCell 可用,不用費心去做 Mock。而那個特別的 appendRow() method,只做了個大概,這是想到等一下要用來合併 Table的 "原料",也許用不著也說不定。有了 DataRow 就可以拿來寫 DataTable 了:

//[code snip#6 完成 DataTable]
public class DataTable {
  public DataTable(int initialRow, int initialColumn) {;}
  public void addRow(DataRow row);
  public void appendTable(DataTable tableToCombine);
}

同樣,DataTable 因為有 DataRow 可用,所以寫來很快。而那個 appendTable 在測試的時候,不斷的去 refactor DataRowTest 和 DataRow 的 appendRow() method,以符合 DataTable 的需求 (比如說同樣的 column 數才能合併之類的條件)

從小到大的撰寫,正常來講除非設計的很完善,知道每個小元件該做什麼,才比較好實施,不然常常寫到大的部份才要重翻會瘋掉... 通常 "常識"之類的設計需求,例如本例中的 Table,大多數的狀況 Table 都是由 row 和 cell 組成的;又或者是車子,大部份都有 engine、輪胎等 parts ... 像這樣子的比較適合吧,因為這種架構都很固定了。

回到正題.... test-first 在這裡也發揮了功能: 我寫完這 DataCell, DataRow, DataTable 時,完全沒有做任何的 debug ! 每一個 class 都是建立在完整測試好的子元件上。當 DataRow 的 appendRow() 不夠 Table的 appendTable 用時,先增加/修改 DataRowTest 的 assertion,然後再修改 DataRow 直到綠燈亮為止。這樣確保 appendTable 去call appendRow時一定不會出錯,也就是撰寫 test 永遠比 refactoring 先做。我雖然不知道如果沒寫 test 的話會發生什麼奇怪的臭蟲,不過這種不用 debug 就寫完程式,這可是寫程式以來還沒遇過的事啊~~~~ 而且完成度還很高 (有一堆 test 掛保證 :-)

不論是 bottom-up, top-down, test first 都有地方可以發揮功能。top-down 需要用點小技巧來做 isolation,通常寫完之後會留下一堆 mock 或 dummy class ,而 bottom-up 比較不會,但是需要事先良好的設計。而且等到子元件越疊越大時,還是得做 mock,不然建立 fixture 的時候會吐血 (也可以用 Object Mother pattern 來解決啦)

Conclusion.... I have been test infected !

2 Comments:

At 11:36 下午, Anonymous 匿名 said...

不錯的文章,你真的寫程式沒多久嗎?
我看了你的文章幾篇blogs後,真是有相見恨晚的感覺....
你應該在testing方面滿有經驗的
你遇到的問題,有不少我也曾有類似的經驗
有些是最近才知道有這些solution的,雖然不知不覺中,
我也是以類似的方式解決了,可能每個程序員都有大致上相同的思維吧
因為一樣的問題,大概好一點的解法就那幾種吧,
像是object mother pattern,最近在refactory我的test data source時,整理到最後,就變成了object mother pattern,不過,這個pattern的名字,我倒是最近在從Martin Fowler的Articles-
Mocks Aren't Stubs (http://www.martinfowler.com/articles/mocksArentStubs.html)看到
雖然他的文章中對interaction-based的testing有較高的評價,但他也說他自已還是比較prefer state-based的esting,我也是 :-)
因為之前用了jmock跟easymock,都覺得用起來沒stubs來得"有感覺",可能是受了kent back的tdd這本書的影響吧,喜歡用fake method,然後再一步步讓他變成implement method.

你的文章雖然不多,但每一篇都滿值得看的,可惜回留言的人不多@@

............................................ 庄腳人 留

 
At 2:19 上午, Blogger ingramchen said...

幸會幸會 :-)
以前斷斷續續學過 Fortran 和一點點的 c++啦,後來跑到半導體業後就沒寫了(過了四年囉~),去年才因為一些因緣重新從 Java 學起。資歷等於是只有一年,真的很淺呢~~

anyway... test 的話我都一直有在看書,像是 Junit in Action, Unit test in java, 還有 XUnit 的網站(http://testautomationpatterns.com/index.html 這個網站超棒) ,然後再包括 Object Mother 那份 paper。然後加上不斷的實際演練,才漸漸有一點想法。Fowler 的那篇文章我也看過了,很訝異他竟然不喜歡 interaction-based 的 test 。不過這一點看法似乎我們大家都一樣,我自己也是比較 prefer state-based 的做法。interaction-based 的用起來真的是毛毛的,因為寫到後來,會測到兩個 class 之間的底層互動。如果兩個 class 間的 interface 不是很穩定的話,refactoring 起來也是很痛苦的。easymock 之流的我暫時是不打算碰了,原因除了 "沒感覺" 之外,還有更重要的是用了 team 裡面的人看不懂啊... 目前才在推廣 test-first 的觀念給 team member的階段,更深入的東西還不敢大量採用。

從開始寫 unit test 到現在還不到四個月呢 (看之前的blog就知道囉) 還是個菜鳥的啦~~ 往後還有 automatic test/continuous integration 這種大題目在等著咧,有機會在一起切磋吧~~

 

張貼留言

<< Home