2011年9月23日 星期五

我的TDD經驗(1)

從T社換到I社之後,一直在一個三人團隊做研究計畫。我們三個很早就說好儘量互相協助,包含寫程式也一起 pair programming,並利用test driven development(TDD)。我一直以來都是用TDD,另外兩位同事只是久聞TDD之名,不曾見過其人,所以當我使用各種技巧(讓本以為無法測試的程式變得可以測試、從很簡單的小功能寫成完整功能、...)寫出程式的時候,同事臉上隱約出現不可置信+恍然大悟的表情。

話說TDD我也是有一段漫長的自學過程...

凡是總有第一次

應該是八年前也是我工作的第一年,當時的公司有開個讀書會,讀的書就是Test Driven Development: by Example。參加讀書會有免費的書可以領(一本$33 USD耶!),中午至少有八次免費便當吃,二話不說馬上報名。讀書會結束之後,覺得書本內容很有道理(其實看每本書都很有道理...),又剛畢業退伍還不太會寫程式,既然學了這個方法,就馬上把TDD用在工作上。

那時要寫的功能是「把檔案神不知鬼不覺傳到遠端電腦,執行它,並且取回執行結果。可以對整個網域或是某個IP範圍的電腦做這樣的事,最後彙整執行結果,並傳回給caller。」聽起來像是在寫電腦病毒,不不不,我可是在防毒軟體公司工作呢!第一次用新學的工具方法,一定是跌跌撞撞。TDD也是,書上講了很多,上手後只記得TDD五拍子:
  1. 先寫測試程式
  2. 執行所有測試,最新的測試會失敗
  3. 再寫程式,讓所有測試成功
  4. 看看有哪裡寫得不好,改好它(就是refactor啦~好險我更之前有先讀過refactor)
  5. 回到第一步 (不是說不要用goto嗎?)
因為不懂得適當切割、不懂怎麼decouple、怎麼隔絕external dependency,所以我的測試需要真正環境的搭配 – 要有台電腦讓我丟執行檔、有執行檔能讓我遠端執行、執行檔要能產生某種格式的output – 這麼麻煩,但我還是硬踩著TDD五拍節奏把程式寫出來了。寫出的測試「兼具單元測試與功能測試的特色」(註一),但過程忍不住的彆扭,有種便秘的感覺,跟現在寫文章的感覺差不多。跌跌撞撞寫出的程式,卻得到測試部門相當不錯的回應。我記得可能在component test中只被找出了一個沒有處理null pointer的重大缺陷以及二三個建議修改事項而已。接著我撰寫其他的元件,也持續以TDD開發,到了年底的績效評量,主管提到我採用TDD而且品質不錯,所以給我不錯的評價。

與Legacy Code共舞


既然得了甜頭,我也就繼續TDD下去了。但只會這五拍子,依然不過是個比三流好一點的工程師而已(三流工程師根本不知道自己的程式會不會動,我有做測試知道它會動,不動也修到會動,所以比三流好一點)就在此時,我被抓去做另一個專案了。前一個專案是從頭開始寫,沒有任何既有的程式碼(這個機會很少耶);新的專案就是從既有產品(稱之為產品C好了)的code base新增功能。

產品C博大精深,既有原始碼又少有搭配的單元測試,每次改code都會擔心改出一堆bug;缺乏單元測試的legacy code還有個特性,一個class常擁有一個以上的responsibility,每個responsibility又帶入一些dependency,最後使得這個class和別的class綁定(coupling,常譯為耦合,這裡借用wow中的術語)的非常嚴重。為了要規避這如義大利麵般糾結的舊程式,我學會了在TDD時利用 interface 來分隔新舊程式,使其無 dependency,再用 adapter 讓舊程式得以接軌新程式。

靠著interface + adapter 101 招,讓我得以不被『義大利麵』淹沒。

從哪裡著手?

TDD大概就這麼一回事,說完收工。但回到座位要真的開始TDD,還真不知如何下手呢!Test Driven Development: by Example第一部的範例是這樣入手的:範例要寫一個貨幣轉換(例如美元對法郎)的程式,從第一章開始就列出所有待辦事項(todo list),裡面有包含所有該完成的功能,然後作者就從中挑了一個項目開始寫起。我後來發現,這整件事是頗有學問的...。

再次搬出「把檔案神不知鬼不覺傳到遠端電腦,執行它,並且取回執行結果。可以對整個網域或是某個IP範圍的電腦做這樣的事,最後彙整執行結果,並傳回給caller。」這個例子,要怎麼把它變成todo list呢?

我是這樣做的:這把這『故事』先按步驟順序分解:

  • 把檔案傳到一台遠端電腦,遠端執行檔案,並且取回執行結果
  • 把檔案傳到很多台遠端電腦,遠端執行檔案,並且取回執行結果
  • 用10.10.10.x~10.10.10.y 指定IP 範圍
  • 用10.10.10.0/24  指定IP 範圍
  • 最後彙整執行結果,並傳回給caller

其中「要把檔案傳到多台電腦...取回執行結果」,我直覺想到可以先從一台電腦做起,所以我多列了一個「要把檔案傳到一台電腦...取回執行結果」。

接著,我覺得這個故事最核心的是,「要把檔案傳到遠端電腦...取回執行結果」這件事,所以我會繼續往下分解。這裡面我最不熟的是遠端執行(包含傳送、執行、取回結果),而且遠端執行需要真的有遠端電腦,這個external dependency不好化解,我決定先把遠端執行單獨拆出,並讓「要把檔案傳到遠端電腦...取回執行結果」這件事更抽象化一點:

  • 遠端執行(傳送、執行、取回結果)
  • 執行一項工作
  • 執行多項工作
  • 用10.10.10.x~10.10.10.y 指定IP 範圍
  • 用10.10.10.0/24  指定IP 範圍
  • 最後彙整執行結果,並傳回給caller

先說獨立出來的「遠端執行」,它是核心中的核心。其他項目還可以凹,例如10.10.10.x~10.10.10.y不做改用10.10.10.0/24也死不了,但遠端執行可不行。所以一般我會停在這裡,去搞清楚到底怎麼做、甚至真的寫出來再往下走。另外,「遠端執行」若沒有環境是沒法測試的,所以我不會強求用TDD開發它;但是,如果我能力所及有辦法弄出一個可測試的環境,我就會TDD。

回來看最新的todo list,分割和抽象化之後出現了「執行一項工作」,它本身沒有單獨存在的必要(實在太抽象啦),直接併入「執行多項工作」。這時我心目中也有一個設計的雛型了:大概會有一個 thread pool 在執行工作,「遠端執行」是一種工作,多台電腦就會是多個遠端執行的工作。所以「指定 IP 範圍」的重點會是產生多個遠端執行的工作,丟進thread pool。至於「彙整執行結果」還不清楚,先擺著看辦吧。

到這裡我應該已經迫不急待準備動手了,假設遠端執行已經沒有讓我不安的東西了(最好是被隔壁同事寫完了),那我會想從「執行多項工作」開始,因為它是剩下的項目裡比較骨幹的部份,骨幹做好,長肉就方便了。下面是剛開始的測試:

void testRunSomeTasks() {

    DummyTask t1 = new DummyTask();

    DummyTask t2 = new DummyTask();



    TaskRunner taskRunner = new TaskRunner();

    taskRunner.add(t1);

    taskRunner.add(t2);

    taskRunner.start();

    taskRunner.waitAll();

    ASSERT(t1.hasBeenRun());

    ASSERT(t2.hasBeenRun());
}


以上就是我著手寫程式的模擬經過。

待續...

1 則留言: