l

2019年3月18日 星期一

落實TDD的三個難題(中):通用語言的建立

March 18 17:02~18:11

▲廣義的說,TDD、ATDD、BDD、SEB都是同義字。


難題二

落實TDD/ATTD/BDD/SBE的第二個難題就是通用語言的建立。傳統TDD(先寫失敗的單元測試)好像只是開發人員自己的事情,它被降級為一種開發方式的選擇(Test First VS. Code First),一種技術議題,與商業無直接關係。

但後來的TDD,也就是冠上ATTD/BDD/SBE之後的TDD,強調透過開發團隊與領域專家的合作,以「舉例」的方式一起釐清需求與商業價值

這裡說的領域專家,可能是Scrum團隊的Product Owner,或是任何具備問題領域知識的stakeholders。這是一種頻繁、高互動性且非常燒腦的活動,而不是單方面由Product Owner寫好user story然後在sprint planning meeting「宣達聖旨」給團隊的那種溝通模式,更不是瀑布式開發那種「文件丟過牆」的溝通模式。

***

通用語言

通用語言(Ubiquitous Language)是領域驅動設計(Domain-Driven Design;DDD)所提出的觀念,意指「在一個特定領域中,所有人所固定使用的術語」。例如,在貓奴這個特定領域(bounded context),大家對於乾乾、濕濕、罐罐、浪浪、小橘、賓士、虎斑、三花、玳瑁、麒麟尾、鏟屎官、聖上、皇后、離胺酸、貓砂、逗貓棒、結紮、貓毛、掃地機器人等名詞有著高度共識。一群貓奴聚在一起,用他們的「通用語言」可以非常有效率溝通而且比較不會造成誤會

這個概念,應用在軟體開發上面,當團隊建立了良好的的通用語言,問題領域的知識可以直接出現在解決方案領域,商業人士可以直接和技術人員用相同的語言溝通,表達商業邏輯。更進一步,在實作軟體系統時,這個通用語言將直接反應在程式碼裡面,也就是ubiquitous language in code。如此一來,程式將變得更加容易理解與維護,也更容易擴充。這是一種讓軟體變軟,擁抱改變的做法

***

通用語言和TDD有什麼關係?

在〈落實TDD的三個難題(上):領域模型與軟體架構〉中Teddy提到建立領域模型對於TDD的重要性,而建立通用語言的過程,同時間幫助團隊建立領域模型,兩者相輔相成。

只要Teddy聽到有朋友「宣稱」採用TDD開發軟體,Teddy一定會問對方:「你們的需求如何產生(如何撰寫失敗的驗收測試)?」如果答案是「Product Owner寫好給我們照做」,那麼落實TDD的程度就還有不少改善的空間。

如果答案是「透過頻繁地與Product Owner溝通,討論列出需求中的重要例子(key examples)」,那就比單向的接受user story要好很多。如果答案是「透過與Product Owner以及stakeholder的討論,以舉例的方式,建立共同討論的語言,並以此為基礎來撰寫程式」,那就可以獲得一張「好棒棒」貼紙。

***

結論

TDD是透過先撰寫測試案例來釐清需求或規格,之後再考慮如何開發程式的一種設計方法。套用建築師Alexander的模式框架,這是一種「先決定Context,再決定Form」的設計方法。

為什麼網路上的TDD範例與TDD Kata你都做得嚇嚇叫,公司的專案做起來卻讓人很想睡覺?很簡單,因為前者具有定義清楚的Context,而後者的Context非常模糊。

一般來講,Product Owner與stakeholders擁有比較多的領域知識,換句話說他們比較了解Context。缺少這些角色的幫忙,開發人員很難獨自透過TDD的方式自行決定Context,這也是許多人在真實專案中落實TDD所遭遇到的困難。

在許多公司,Product Owner與stakeholders覺得「撰寫失敗的測試案例是開發人員的事」,人家不願意理你啊。

難怪你的TDD又變成XD了。

***

友藏內心獨白:不能放任開發人員自由發揮,結果會很恐怖。

2019年3月15日 星期五

落實TDD的三個難題(上):領域模型與軟體架構

March 15 07:11~09:11


緣起

幾年前有一位泰迪軟體忠實學員問Teddy:「我上過TDD的課,但回公司後卻不知道該怎麼落實,為什麼?」當時Teddy無法回答這個問題,因為:(1)他上的TDD課程不是Teddy教的,不知道他學了什麼;(2)Teddy以前工作上開發的軟體,只有不到10%是採用TDD,其他大部分都是用傳統OOAD,code first(先寫production code再寫test code)的方式,所以沒在工作上遇到全面落實TDD的問題。

雖然工作上TDD用的不多,但Teddy寫了很多測試案例,也做了持續整合,算一算也有16年的時間。這幾年因為教學需要,投資大量時間在TDD/BDD/SBE以及DDD/Clean Architecture上面,直到最近才慢慢有種可以清楚回答N年前這個問題的感覺。

***

例子太小

不少人都是透過網路上的例子或是TDD Kata來學習TDD,這些例子大多具備以下特點:

  • 所需物件很少:只有單一或少量物件、例如著名的Bowling Game Kata,只需要一個Game物件就搞定。
  • 商業邏輯明確:上述提到的Bowling Game Kata,或是計算不同方式的郵寄費用(平信、掛號、國內快捷、國際郵件)、商品費用(一般客戶、VIP、大量採購、特價商品)等例子,它們要解決的問題「商業邏輯」都非常明確,很容易透過TDD,採用逐次完成每一個例子的方式來實作完整商業邏輯。
  • 不須考慮架構:因為例子小,邏輯清楚,所以也不需要考慮軟體架構的問題。

以上特性,對於一個「以學習TDD為目的」的例子來說,原本都不是問題,反而是優點。因為例子很專注在特定的小問題上面,所以學習者可以在短時間內把握TDD的精神:

  • 寫一個失敗的測試案例
  • 用「最笨」的方式撰寫程式碼讓測試案例通過
  • 重構程式

***

問題在哪裡?

當使用者學了TDD要實際應用在工作上的專案,此時卻發現,實際要解決的問題放大了N倍。不只物件變多、商業邏輯複雜,連帶著軟體架構也需要一起考慮。這些都是在學習TDD階段沒有冒出來的因素(forces)。

奇怪,為什麼看別人TDD,只要透過撰寫測試案例,物件與介面好像信手拈來就有。換成我來TDD,就D不出來,最後只能XD。


▼如下圖所示,這個問題隨著驗收測試開發(ATTD)、行為驅動開發(BDD)、實例化規格(SBE)等方法,將原本傳統TDD「先撰寫失敗單元測試」提升為「先撰寫失敗驗收測試」之後稍有緩解。驗收測試提供TDD一個更大的Context (背景、脈絡),讓開發人員擁有更多的資訊來「隔空抓藥」,透過測試案例描述物件以及物件之間的互動關係。


▼Specification By Example範例


但就算有了驗收測試,整個系統的全貌還是無法清楚呈現,透過每次撰寫失敗測試案例來完成系統依舊屬於由下而上的開發方式。

由下而上的開發方式最大的問題就是:「最後兜出來的系統很容易長歪掉。」看到這裡鄉民們可能會想:「敏捷開發是一種迭代與增量的方法,這不也是一種由下而上的方式?難道敏捷開發也很容易長歪掉嗎?

沒錯,完全正確。這也是為什麼現在敏捷開發流行採用「影響力對照(impact mapping)」與「用戶故事對照(user story mapping)」協助團隊關照系統全貌。撰寫程式之前,不管這個程式是test code還是production code,如果對於所開發的系統缺少一種「整體的感覺」,最後的系統設計就很容易長歪掉。

***

怎麼辦?

其實答案很簡單,就是準備「剛剛好的事前設計」(just enough up-front design)。問題是怎麼拿捏這個「剛剛好」?

▼如下圖所示,對照OOAD與TDD/BDD/SBE的做法,後者少了強調「建立領域模型」(domain model)這個步驟,讓鄉民以為domain model裡面的物件,不需要特別分析與設計就會自然而然隨著撰寫失敗的測試案例而冒出來。就算是剛開始找到的物件不洽當,反正最後總是可以透過重構來改善設計品質。


能力強者如Kent Beck或Uncle Bob等級的人物,在腦海中已有某種 皇輿全覽圖「軟體全貌地圖」,因此可以信手拈來得到合適的領域物件。就算設計不小心歪掉,後續採用重構來改善系統設計品質對他們而言也不是難題。大師們只需極小化的事前設計便可順利透過TDD完成系統,但一般大眾畢竟敏捷性沒有那麼高,所以適量的事前設計有助於TDD。

扯了這麼久還是沒講到具體解法。以下是Teddy建議的方向:

  • OOAD:如果鄉民們學過OOAD,可以參考80-20原則。找出系統中20%最優先的use case或user story,花一點點時間地建立domain model。有了這個domain model,對於後續將「失敗測試案例」轉成test code會很有幫助。


▼cleanKanban系統的領域模型


  • DDD:參考領域驅動設計方法(Domain-Driven Design;DDD),建立domain model與通用語言(Ubiquitous Language)。有這兩項「致命武器」,後續不管你想「怎麼D」都可以得心應手。


▼透過事件風暴(event storming)找出領域事件與建立領域模型


  • Clean Architecture:軟體架構百百種,屬於「插件式架構」的Clean Architecture,因為具備高度擴充性與可測試性,很適合作為各種軟體的「預設架構」。搭配Clean Architecture,撰寫TDD的失敗驗收測試直接對應到呼叫Use Case,而Clean Architecture的Use Case有著固定的結構,需要定義清楚的Input與Output介面。也就是說,Clean Architecture限縮了開發者的「選擇性」,而讓TDD的開發的工作變得更簡單。

▼搭配Clean Architecture採用TDD開發所撰寫的失敗驗收測試。

***

結論

敏捷開發與TDD都不鼓勵大量事前設計(Big Up-Front Design;BUFD),但並不是說不需要任何事前設計直接「帶著鋼盔往前衝」就可以攻克敵軍山頭。合適的事前設計,可以幫助開發人員釐清目標,支撐後續迭代與增量式開發活動,讓整體設計慢慢湧現。

***

友藏內心獨白:緣分到了,問題就想通了。


廣告

對於Clean Architecture搭配TDD與Event Storming(事件風暴)有興趣的鄉民,可參考泰迪軟體的【Clean Architecture這樣學就會了實作班】,2019年4月份課程已確定開課。

2019年3月14日 星期四

無用之用

March 14 16:37~18:06


The Timeless Way of Building》這本書從2003年5月30日入手起算到現在已將近16年。剛到手時幾乎每天都在傻傻地讀,因為看不懂作者想要表達的真正意思,只能硬著頭皮念下去。這本書的英文用字與文法不算太難,而且有中文版可以對照著看,所以讀不懂的主要原因倒不是因為語言隔閡,而是無法理解作者背後的整體思想。作者的腦袋裡不知道裝什麼,真是太奇葩了XD。

有人可能會問:「念資工的幹嘛去讀建築的書?當然看不懂啊。」當初因為博士論文想研究設計模式,指導教授說:「要研究pattern,不能只看GoF 的《Design Patterns》這本書,要從源頭去研究pattern發明人Alexander的作品。

就因為這句話,Teddy立刻上Amazon買了《The Timeless Way of Building》、《A Pattern Language》以及《Notes on the Synthesis of Form》各兩本。一本自己讀,另一本給指導教授,「暗示」指導教授也要一起讀。總不能只有Teddy一人受苦啊 XD。

***

印象中,過了一段瞎子摸象的日子,後來慢慢地有點感覺,可以將書中的一些講法「硬套」在軟體開發上面,有種重見光明的感覺。

數年後,一直到博士班畢業,Teddy都還沒把這本書整本看懂。又過了幾年,泰迪軟體成立後因為要賺錢生存下來,在設計【Design Patterns這樣學就會了–入門實作班】教材時,Teddy特別在第一天的課程介紹Alexander的方法。幾年下來,「Design Patterns這樣學就會了–入門實作班」教了20幾次,不斷地修改教材內容,對這本書的體會又更深了一些。

***

五年多前Teddy開始讀《Domain-Driven Design》(DDD),一開始也搞不清楚DDD到底在搞什麼。表面上看起來,就是另一種物件導向分析設計(OOAD)的方法啊,但仔細一看,和傳統的OOAD味道又不一樣。那是個有點陌生但又熟悉的味道,耶,原來DDD是一種pattern language!作者Eric Evans很顯然也受到Alexander的影響。有了這層認識,再回頭看DDD的眼光就不一樣。不但看得更深入,也可以重複使用以前讀Alexander書本的那些知識。

這就是Teddy之前說的:「有九陽神功護體,學什麼功夫都快。

***

兩個多禮拜前Teddy因為要設計【Clean Architecture實作班】課程教材,用TDD/Specification By Example(SBE)加Clean Architecture加DDD的Event Storming、Ubiquitous Language與Aggregate等技術,幾乎「無痛地」很快就把範例做好。之前對於TDD/SEB有一些沒想清楚的地方,在無形之中居然豁然開朗。這並不是因為這兩個禮拜Teddy吃了天山雪蓮突然功力大增,而是之前下的功夫點點滴滴累積,剛好在這個時間點「因緣成熟」而豐收

如果沒讀過Alexander的書,也許這一切「好事」都不會發生。

感恩seafood、讚嘆seafood。

***

友藏內心獨白:還好在台灣這本書沒什麼人讀,少了很多競爭對手。

2019年3月12日 星期二

關於時間的測試

March 12 07:51~09:07


時光一逝永不回

程式中如果有使用到「時間」,對測試來說是一個很傷腦筋的問題。例如,在看板系統中,想測試一個工作項目(work item)的lead time(從開工到交貨的時間)或cycle time(在某個或某些工作階段停留的時間),如果採用手動測試,測試人員需要不斷修改電腦時間,以便模擬出工作項目停留在不同工作階段的狀況

在〈對付時好時壞的測試案例(5):Time〉Teddy曾經談過這個問題,今天要從程式碼的角度再談一次。

***

程式範例

為了製作「Clean Architecture實作班」課程範例,Teddy最近忙著開發一個看板系統軟體稱為cleanKanban。Teddy參考領域驅動設計(Domain-Driven Design;DDD)的作法,套用Aggregate設計模式。一個aggregate將一小群物件包裝在一起,最上層的物件稱為aggregate root,負責維持不變量(invariant)與交易一致性。

不同Aggregate之間的狀態透過領域事件(domain event)保持同步,下圖AbstractDomainEvent類別代表領域事件的抽象類別,其中occurredOn屬性紀錄事件發生的日期與時間。

上圖中AbstractDomainEvent類別直接透過 new Date()獲得日期物件,這種寫法要撰寫自動測試就很傷腦筋。

學過測試替身Test Double(1):什麼是測試替身?〉的鄉民應該會想到以下兩種解法:

  • 相依性注入技巧,AbstractDomainEvent不要自己產生Date物件,而是讓呼叫它的物件傳入。這種做法有兩個潛在問題:
    • 需要改程式,破壞了既有程式的介面。如果AbstractDomainEvent已經有很多子類別實作,異動到的程式就比較多。
    • 客戶端比較難用。很多領域物件(domain object)都會產生領域事件,代表事件產生時間的occurredOn屬性如果只是因為測試的原因需要外部注入,平添客戶端的麻煩。
  • 採用mock object,透過mock object在測試時直接攔截 new Date()呼叫,注入特定日期。這種做法不需要改程式,在測試案例上動手腳即可。詳細討論可參考stackoverflow的這篇文章

***

透過第三者

除了上述兩種解法,還有一種方式就是透過第三者得到時間物件。撰寫DateProvider類別,透過它間接獲得日期物件。DateProvider 允許使用者注入一個日期物件,在測試模式下可注入特定日期達到模擬不同日期的自動化測試目的。

▼修改AbstractDominEvent的建構函數,透過DateProvide.new() 傳回日期。


▼在測試案例中,將特定日期傳給DateProvider,可以模擬領域事件發生在特定日期的情況。

***

結論

今天談到關於日期測試的三種方法,各有優缺點,視不同情況可能都會派上用場。DateProvider的缺點是,萬一有人要搞破壞在production code裡面呼叫到DateProvider.setDate(),注入一個特定日期,系統狀態可能就整個錯掉。這個問題可以透過把DateProvider寫得更「精緻」一點,只允許在測試環境被注入日期來改善。

關於更多測試技巧,歡迎參考泰迪軟體的【單元測試這樣學就會了實作班】。

***


友藏內心獨白:有月光寶盒才可以穿梭時空。

2019年3月6日 星期三

最新課程:【Clean Architecture實作班】

March 06 14:00~16:20


今年泰迪軟體新規畫一門兩天課程:【Clean Architecture實作班】(簡潔架構實作班),這是去年「Clean Architecture嘴砲班」的進化版。今年的新課程,打嘴砲練練嘴上功夫還是必要的,此外增加了一個完整的實作練習,讓大家動動腦與練練手上功夫。

關於這個練習題目Teddy規劃了超過一整年,去年設計好的題目經過實驗發現整體來說有點太難。於是今年Teddy從新設計一個練習範例,除了學習Clean Architecture以外,還同時兼顧TDD/BDD/SBE,並借用DDD(領域驅動設計)的Ubiquitous Language(通用語言)Event Storming(事件風暴)AggregateEntity來幫助學員分析domain model(領域模型)的物件邊界。

今天簡短介紹一下課程範例。

***

看板系統需求

課程範例要開發一個看板系統(Kanban System),稱為cleanKanban。看板範例如下所示,由不同工作階段所組成,以下圖為例,有待辦事項、分析、實作、測試、可佈署等五個工作階段。

▲看板範例


每個工作階段還可以分為子工作階段,以上圖為例,待辦事項細分成「想法」與「Top 6」,分析階段細分成「進行中」與「完成」。

使用者將工作項目移入不同的工作階段,以視覺化方式追蹤工作進度。每個工作階段可以設定WIP上限(Work In Progress Limit),當工作項目已達WIP上限時,就不可以再拉入工作到該工作階段。

最後,系統要計算工作項目在每個工作階段所停留的時間,稱為Cycle Time,以及Lead Time

結論就是,cleanKanban系統要支援看板系統三原則:

  • 視覺化(工作流程)
  • 限制WIP
  • 管理工作流

***

測試驅動開發/行為驅動開發

Clean Architecture的重點之一就是將系統分層,如下圖所示,由內而外分別是:Entity(Domain Model)Use CaseInterface AdapterFramework and Driver

▲Clean Architecture,圖片來源在此


課程採取測試驅動開發(TDD/BDD/SBE)方式,首先寫出規則(Specification)例子(Example)


看到這裡鄉民們可能會想:「要用BDD或Specification by Example方法,需要使用Cucumber或SpecFlow工具嗎?

答案是:可以使用但先不要用工具之前,先練習用腦,以免在學習過程中被工具綁住。本練習只需要最簡單的xUnit單元測試工具即可 。


▲用JUnit撰寫第一個失敗的驗收測試


▲因為要套用Clean Architecture,最後的驗收測試長成這樣,直接呼叫use case。

***

事件風暴(Event Storming)

Clean Architecture非常重視領域模型的建立,如果有學過物件導向分析與設計(OOAD)的朋友,可以沿用原本OOAD的技巧來建立領域模型。在這門課中,Teddy將介紹在領域驅動設計很流行的事件風暴,來協助建立領域模型與通用語言。

***

專案結構

最後開發出來的專案結構,反應Clean Architecture的三大原則:

  • 分層原則
  • 相依性原則
  • 跨層原則

***

課程費用

  • 原價$22,000元。
  • 早鳥優惠: NT$18,900/人(2019年3月19日前報名並完成繳費。)
  • 2人團報:$17,900 元/人。
  • 嘴砲班舊生優惠:$9,900 元/人(僅限五名)。

課程網址:http://teddysoft.tw/courses/clean-architecture/

上課地點:台北市(近台北車站)。2019年4月19、20日(五、六),09:30-16:30,共12小時

image

***

友藏內心獨白:一個課程三種享受—Clean Architecture、TDD與DDD。

2019年2月27日 星期三

鍛鍊你自己的九陽神功

Feb. 25 23:07~23:34


幾年前在上【單元測試這樣學就會了實作班】的時候有一位學員問Teddy…

學員:我在公司組了〈xUnit Test Patterns: Refactoring Test Code〉讀書會,但是效果不好,還是看不懂書中在講什麼。

Teddy:先撇除英文的因素,我覺得妳們看不懂是很正常的。首先,這本書在講測試,但你們很多測試的基本知識都缺乏,所以看不懂。其次,這本是是用模式(pattern)的方法來講測試,你們也不懂pattern,像是pattern的六大格式,所以讀起書來抓不到重點以及各個pattern之間的關聯性。

***

上禮拜四中午跟學生meeting,指導教授也來參加。我跟他們快速介紹event storming與DDD的aggregate和repository設計,前後大概花了10~15分鐘。指導教授當然是完全聽懂,學生「表面上」也聽懂了,但Teddy猜想實際上學生應該還不太懂,因為這些東西平常Teddy大概至少要花1~2小時來解釋。

在身邊認識的人裡面,大概也只有跟指導教授討論事情,可以享受光纖通訊的高速頻寬。大部分遇到的學生,溝通頻寬都還處在用modem撥接的速度。

***

這個時代大家做事情都在講求快速,但在讀書這件事上面Teddy動作很慢。經典好書,不是買回家之後「假裝看完」就沒事了,而是要看懂。這個過程,絕對不是看過1~2遍那麼簡單,至少看個5~6遍都是很稀鬆平常的事情。

把一個問題徹底弄清楚,有時候需要2~3年的時間的反覆迭代、沈澱、發酵。緣分到了,問題也就想通了。

知識的層次就是這樣一層、一層的累積出來。馬步扎得深,成為日後吸納新知的加速器。電影倚天屠龍記裡面張三豐說:「有九陽神功護體,學什麼功夫都快。」Teddy年輕時第一次聽到這句話,覺得哪有這種事,完全不符合邏輯,鬼扯。

老了之後才發現,這句話是真的XD。

***

友藏內心獨白:九陽神功就是你的學習力。

2019年2月26日 星期二

從領域驅動設計看Tennis Kata(下)

Feb. 25 18:20~19:38

▲人神之間的共通語言XD


Tennis Refactoring Kata介紹

鄉民們可以在這裡找到10幾個語言的Tennis Refactoring Kata程式碼。以Java語言為例,專案結構如下,一共有五個Java檔案:


  • TennisGame:定義TennisGame介面,讓TennisGame1~TennisGame3實作。


  • TennisGame1:第一個版本的重構對象,裡面有幾個很明顯的壞味道(Bad Smell),也有幾個小地方稍微把邏輯整理一下就可以完成重構任務。


  • TennisGame2:第二個版本的重構對象,這個版本的程式碼更長,用最無腦的方式把計分規則逐一展開,想要模擬不太會寫程式的新手所完成的作品。乍看之下有點頭大。但只要做過第一個版本,你已經知道網球遊戲的計分規則。只要把成對的if逐一合併之後,重構就大致完成。


  • TennisGame3:第三個版本的重構對象,這個版本的程式最精簡,只要套用rename與extract method這兩個重構就差不多可以搞定。


  • TennisTest:測試案例,因為這個kata的目的是要練習refactoring,所以範例程式提供了完整的測試案例。鄉民們在練習的時候,只要測試案例通過,就代表沒有改變程式原本的外部行為。


Teddy今天不是要討論如何重構這三個class,重構的活動就交給鄉民們自行練習。

***

和DDD有什麼關係?

▼下列程式為TennisGame2的(一種)重構結果。


鄉民們可以發現,經過重構後計算分數的邏輯分成四個階段:

  • BeforeDeuce:在deuce之前。
  • Deuce:發生Deuce的時候。
  • Advantage:差一分就獲勝的時候。
  • GameOver:這一局結束。

上述程式當然還可以進一步重構,眼尖的鄉民也許發現可以套用State設計模式,把getScore函數簡化成只有一行:

public String getScore() {
         return state.getScore();
     }

這整個過程是一小步、一小步重構之後,如果幸運的話最後走到套用State設計模式,讓自己看起來好棒棒。但是,鄉民們也可以從領域驅動(DDD)設計的角度來思考這個重構。假設鄉民們找到網球專家跟你一起討論需求,從網球專家的互動中,你們整理出以下domain model:

此時你們只關注Game與Game的計分方式。因為Game的計分方式與狀態有關,不同的狀態有不同的分數顯示邏輯。在與領域專家討論時,你們決定把Deuce之前的狀態稱為Initial,其他三個計分狀態分別為Deuce、Advantage、GameOver。

到這邊為止,有兩種可能的方式進行重構:

  • 小步快跑:繼續採用小步快跑的方式,慢慢重構系統,不要直接決定就是要套用State設計模式,除非程式的結構已經很明顯表達出這種可能性。
  • 重構為模式:與領域專家討論過後,你對於問題領域更佳清楚。domain model已經暗示了可以套用State設計模式,於是你決定「用跳的」,直接把程式重構為套用State設計模式。

以下為套用State設計模式後的檔案


套用State設計模式的TennisGame。Teddy把state切換邏輯寫在TennisGame身上,也可以改成放在每一個具體狀態類別身上。

***

結論

用最初淺的方式來看DDD,花點時間整理domain model,並建立ubiquitous language。無論採用小步快跑的重構,直接重構成設計模式,或是採用TDD的方式來開發系統,有一個共同的語言,對於任何方式的軟體開發都很有幫助

***

友藏內心獨白:寫程式有時候不是英文不好不會取名字,而是沒有花時間建立ubiquitous language。

2019年2月25日 星期一

從領域驅動設計看Tennis Kata(上)

Feb. 25 12:39~14:00

▲Eiffel正在修練睡夢讀書法Kata


Tennis Kata

Kata,中文翻譯成套路。在敏捷開發領域中,kata指「被設計用來練習特定技巧(特別是程式設計)的題目與參考答案。」

最近Teddy在修改【軟體重構入門實作班】與【單元測試這樣學就會了實作班】教材內容,花點了時間重新練習了幾個kata。今天想從DDD(domain-driven design;領域驅動設計)的觀點,談一個經常被練習的題目:Tennis Kata。

Tennis Kata有兩個版本,一個用來練習TDD,另一個用來練習Refactoring。透過TDD方式撰寫或直接重構已完成的網球比賽計分程式,分別學習這兩種技能。

一個完整的網球比賽分成:

  • 比賽(Match)
  • 盤(Set)
  • 局(Game)

一場比賽有若干盤,一盤有若干局。詳細說明可參考維基百科。為了簡化起見,Tennis Kata只計算局(Game)的結果。關於局的計分方式,維基百科的解釋如下:

網球每局的記分方法:從0至3分分別為「零」(love)、「十五」(fifteen)、「三十」(thirty)和「四十」(forty)。記分時,發球手的得分在前。因此「30比0」的意思是,發球手贏得2分,而接球手還未得分。

當雙方運動員都得到了3分時,一般叫「平局」(deuce而非「40比40」。在出現平局後一名球手再得一分,被稱為「占先」(advantage),而不再記分。如果在占先的情況下失去一分,就再度回到平局;如占先後再得一分,就贏得一局。

今天先談Tennis TDD Kata,明天再談重構。

***

Tennis TDD Kata

網路上有很多Tennis TDD Kata的範例,Google搜尋時Teddy找了第一筆資料來參考。

作者把用TDD完成Tennis Kata的主要過程記錄下來,但是讀到最後完成的程式,Teddy覺得下列的IsDeuce函數的邏輯好像怪怪的。


▲節錄自Day29 TDD套路經典!? Tennis Game!


如果第一個比賽選手的分數大於等於3,IsDeuce回傳true。但根據網球比賽的規則,deuce有兩個條件:

  • 雙方選手的分數都是3分,或
  • 超過3分之後,在這一局比賽結束前,如果再度平手,也稱為deuce

那為什麼這段程式會寫成判斷 _firstPlayerScore >= 3 ,連是否平手都沒判斷?

繼續往前閱讀文章,Teddy發現作者在判斷是否為deuce的時候,production code長成下面這樣。

 

▲節錄自Day29 TDD套路經典!? Tennis Game!

接著作者用extract method重構,把 _firstPlayerScore >= 3 放到IsDeuce函數,所以造成isDeuce的邏輯變成_firstPlayerScore >= 3。

***

和DDD有什麼關係?

DDD強調domain modelubiquitous language(在限定領域中開發人員與利害關係人一致性使用的語言或是術語),幫助開發人員在系統重構時找出重構目標、重新分派物件責任,最後利用重構逐步將ubiquitous language實作在程式碼中(ubiquitous language in code)。上面這個例子,測試案例雖然都通過,程式也可以動,但是並沒有完全做到ubiquitous language in code。因為deuce的定應,包含在Tennis的ubiquitous language裡面,也就是前面提到的:

  • 雙方選手的分數都是3分,或
  • 超過3分之後,在這一局比賽結束前,如果再度平手,也稱為deuce

而上面的程式範例並沒有正確表達這個邏輯。

***

重構的錯誤

就算不考慮ubiquitous language的觀點,直接把_firstPlayerScore >= 3用extract method改成IsDuece也不洽當。為什麼,再看一次作者原本的程式碼:

 

▲節錄自Day29 TDD套路經典!? Tennis Game!

這個程式碼有兩個if,結構為:

if (_firstPlayerScore == _secondPalyerScore) {

   if (_firstPlayerScore >= 3) {

return “Deuce”;

  }

}

也就是說,deuce的條件必須兩個if同時成立原本作者的程式邏輯是對的,反倒是重構之後沒注意到if (_firstPlayerScore >= 3)其實是存在另一個 if之下,直接extract method反倒讓程式碼與領域知識不一致

如果可以把程式改成下面這樣,isDeuce就符合deuce在Tennis領域的定義,應該比較落實ubiquitous language in code的精神。

***

讓程式碼說實話

程式設計師應該大多認同,程式是寫給人看的,不是寫給電腦看的。落實ubiquitous language in code,你的程式碼就會說人話,而且是實話,不是謊話。

如此一來,可以避免程式碼和領域知識不一致。程式比較容易理解,開發人員也比較敢修改程式。這也是一種擁抱改變,讓軟體變軟的方法

***

友藏內心獨白:說實話很重要。

2019年2月20日 星期三

透過測試涵蓋率讓重構更有信心(下):特徵測試

Feb. 19 12:59~15:13

▲不管黑貓白貓,能陪睡的就是好貓XD


重構既存系統的難題

前兩集〈透過測試涵蓋率讓重構更有信心(上):分支涵蓋率〉與〈透過測試涵蓋率讓重構更有信心(中):突變測試〉所使用的範例Account類別的withdraw函數,其程式邏輯很簡單,藉由觀看程式碼很容易就可以設計出足夠的測試案例,以支持後續的重構活動。

在實務上,鄉民們經常要對既存系統(legacy system)進行重構。這些既存系統除了沒有自動化測試案例,通常程式都寫得很亂,且缺乏文件,邏輯複雜導致沒人看得懂程式在做什麼。因此,重構既存系統成為一件很困難的工作。因為沒人看得懂程式邏輯,別說重構,連要幫程式補寫測試案例都十分困難

***

特徵測試

有一種測試技巧稱為特徵測試(Characterization Test,又叫做Golden Master Test),非常適合幫既存系統設計測試案例。

特徵測試的想法很簡單:把既存系統當作一個黑盒子,既然沒人看得懂既存系統的程式邏輯,那就幹脆不要試圖去了解它的內部邏輯。只要想辦法把既存系統的實際外部行為(系統執行結果)記錄下來,這份紀錄下來的資料稱作golden master。接下來就可以去重構系統,每次重構之後重新執行一次程式,把執行結果跟原本錄製下來的golden master比對。如果比對結果一樣,就表示此次重構沒有破壞系統原本的行為。

▼特徵測試與golden master。

***

範例介紹

Teddy找了〈Gilded Rose Kata〉當作既存系統範例,待測程式有兩個類別,分別是Item與GildedRose。

▼Item類別很簡單,沒什麼程式邏輯,就是一個簡單的data class(存放資料的類別)。


▼真正棘手的是GildedRose類別,它只有一個updateQuality函數,但是這個程式邏輯真的是很難了解它的明白。


▼這個範例有一個測試案例如下。很顯然光是只有這個測試案例遠遠不足以讓開發人員有信心去重構程式,但就算是把待測程式原始碼讀了好幾回,依然不知道該怎麼設計其他測試案例。

***

Approval Tests工具簡介

接下來的問題是要怎麼產生足夠的特徵測試,以便得到一份足以支持後續重構活動的golden master。在這裡Teddy要使用Approval Tests工具,它使用資料驅動測試案例(data-driven tests,又稱為參數化測試案例)技巧,將每次測試結果全部紀錄下來當作golden master。只要提供的資料(參數)夠多,測試案例的涵蓋率越高,開發人員對於所產生的golden master信心也越高,便可開始進行重構活動。

▼首先打開〈Gilded Rose Kata〉Java版本的程式範例,將下列相依性加入pom.xml檔案中。


▼接著在updateQuality測試案例中加入Approvals.verify(app.items[0].name),這一行可以用來取代JUnit的assertEqulas。


▼執行測試案例,結果居然是錯的?!


▼打開測試案例目錄,發現增加了兩個檔案:

  • GildedRoseTest.updateQuality.approved.txt
  • GildedRoseTest.updateQuality.received.txt


*approved.txt檔案相當於golden master,*received.txt則是此次執行測試案例所得到的程式輸出結果。Approvals會自動比對這兩個檔案的內容,如果內容相同表示程式的行為沒有被改變,測試案例通過,反之則不通過。*approved.txt檔案如果不存在,Approvals會自動產生一份內容為空白的檔案。


▼把*received.txt(左邊)與*approved.txt(右邊)檔案打開,可以發現左邊的內容為foo,右邊是空白,比對失敗,所以Approvals測試案例執行失敗。


▼要讓Approvals測試成功很簡單,只要把*received.txt改名成*approved.txt。


▼再執行一次測試案例即可。

***

產生Golden Master

Approvals工具有一個CombinationApprovals類別,它提供verifyAllCombinations函數,輸入一個待測程式的函數(closure),以及若干個參數,便可以撰寫資料驅動測試案例,然後將所有測試結果記錄下來。


▼先把原本的測試案例用extract method重構一下,獨立成doUpdateQuality method,以便等一下將它傳給CombinationApprovals


▼原本的updateQulaity測試案例變成這樣:

  • 提供給verifyAllCombinations函數的第一個參數是this::doUpdateQuality,也就是剛剛重構後的測試案例。
  • 因為執行doUpdateQuality需要三個參數,分別是name、sellin、quality,所以接著提供Stirng []、Integer []、Interger [] 給verifyAllCombinations函數。
  • 目前只提供一組資料{”foo”, –1. 0},隨著提供的測試資料越多,Golden Master的內容就越完整。


▼只有一組資料{”foo”, –1. 0}產生的*received.txt檔案內容如下。


▼看一下測試涵蓋率,好多程式碼都沒被執行到,還要多增加幾組測試資料才行。

***

▼套用前兩集提到的分支涵蓋率突變測試技巧,最後的特徵測試如下。


▼已達到100的分支涵蓋率,也通過手動的突變測試。


▼最終的Golden Master內容:

[foo, -1, 0] => foo, -2, 0
[foo, -1, 1] => foo, -2, 0
[foo, -1, 49] => foo, -2, 47
[foo, -1, 50] => foo, -2, 48
[foo, 0, 0] => foo, -1, 0
[foo, 0, 1] => foo, -1, 0
[foo, 0, 49] => foo, -1, 47
[foo, 0, 50] => foo, -1, 48
[foo, 2, 0] => foo, 1, 0
[foo, 2, 1] => foo, 1, 0
[foo, 2, 49] => foo, 1, 48
[foo, 2, 50] => foo, 1, 49
[foo, 6, 0] => foo, 5, 0
[foo, 6, 1] => foo, 5, 0
[foo, 6, 49] => foo, 5, 48
[foo, 6, 50] => foo, 5, 49
[foo, 11, 0] => foo, 10, 0
[foo, 11, 1] => foo, 10, 0
[foo, 11, 49] => foo, 10, 48
[foo, 11, 50] => foo, 10, 49
[Aged Brie, -1, 0] => Aged Brie, -2, 2
[Aged Brie, -1, 1] => Aged Brie, -2, 3
[Aged Brie, -1, 49] => Aged Brie, -2, 50
[Aged Brie, -1, 50] => Aged Brie, -2, 50
[Aged Brie, 0, 0] => Aged Brie, -1, 2
[Aged Brie, 0, 1] => Aged Brie, -1, 3
[Aged Brie, 0, 49] => Aged Brie, -1, 50
[Aged Brie, 0, 50] => Aged Brie, -1, 50
[Aged Brie, 2, 0] => Aged Brie, 1, 1
[Aged Brie, 2, 1] => Aged Brie, 1, 2
[Aged Brie, 2, 49] => Aged Brie, 1, 50
[Aged Brie, 2, 50] => Aged Brie, 1, 50
[Aged Brie, 6, 0] => Aged Brie, 5, 1
[Aged Brie, 6, 1] => Aged Brie, 5, 2
[Aged Brie, 6, 49] => Aged Brie, 5, 50
[Aged Brie, 6, 50] => Aged Brie, 5, 50
[Aged Brie, 11, 0] => Aged Brie, 10, 1
[Aged Brie, 11, 1] => Aged Brie, 10, 2
[Aged Brie, 11, 49] => Aged Brie, 10, 50
[Aged Brie, 11, 50] => Aged Brie, 10, 50
[Backstage passes to a TAFKAL80ETC concert, -1, 0] => Backstage passes to a TAFKAL80ETC concert, -2, 0
[Backstage passes to a TAFKAL80ETC concert, -1, 1] => Backstage passes to a TAFKAL80ETC concert, -2, 0
[Backstage passes to a TAFKAL80ETC concert, -1, 49] => Backstage passes to a TAFKAL80ETC concert, -2, 0
[Backstage passes to a TAFKAL80ETC concert, -1, 50] => Backstage passes to a TAFKAL80ETC concert, -2, 0
[Backstage passes to a TAFKAL80ETC concert, 0, 0] => Backstage passes to a TAFKAL80ETC concert, -1, 0
[Backstage passes to a TAFKAL80ETC concert, 0, 1] => Backstage passes to a TAFKAL80ETC concert, -1, 0
[Backstage passes to a TAFKAL80ETC concert, 0, 49] => Backstage passes to a TAFKAL80ETC concert, -1, 0
[Backstage passes to a TAFKAL80ETC concert, 0, 50] => Backstage passes to a TAFKAL80ETC concert, -1, 0
[Backstage passes to a TAFKAL80ETC concert, 2, 0] => Backstage passes to a TAFKAL80ETC concert, 1, 3
[Backstage passes to a TAFKAL80ETC concert, 2, 1] => Backstage passes to a TAFKAL80ETC concert, 1, 4
[Backstage passes to a TAFKAL80ETC concert, 2, 49] => Backstage passes to a TAFKAL80ETC concert, 1, 50
[Backstage passes to a TAFKAL80ETC concert, 2, 50] => Backstage passes to a TAFKAL80ETC concert, 1, 50
[Backstage passes to a TAFKAL80ETC concert, 6, 0] => Backstage passes to a TAFKAL80ETC concert, 5, 2
[Backstage passes to a TAFKAL80ETC concert, 6, 1] => Backstage passes to a TAFKAL80ETC concert, 5, 3
[Backstage passes to a TAFKAL80ETC concert, 6, 49] => Backstage passes to a TAFKAL80ETC concert, 5, 50
[Backstage passes to a TAFKAL80ETC concert, 6, 50] => Backstage passes to a TAFKAL80ETC concert, 5, 50
[Backstage passes to a TAFKAL80ETC concert, 11, 0] => Backstage passes to a TAFKAL80ETC concert, 10, 1
[Backstage passes to a TAFKAL80ETC concert, 11, 1] => Backstage passes to a TAFKAL80ETC concert, 10, 2
[Backstage passes to a TAFKAL80ETC concert, 11, 49] => Backstage passes to a TAFKAL80ETC concert, 10, 50
[Backstage passes to a TAFKAL80ETC concert, 11, 50] => Backstage passes to a TAFKAL80ETC concert, 10, 50
[Sulfuras, Hand of Ragnaros, -1, 0] => Sulfuras, Hand of Ragnaros, -1, 0
[Sulfuras, Hand of Ragnaros, -1, 1] => Sulfuras, Hand of Ragnaros, -1, 1
[Sulfuras, Hand of Ragnaros, -1, 49] => Sulfuras, Hand of Ragnaros, -1, 49
[Sulfuras, Hand of Ragnaros, -1, 50] => Sulfuras, Hand of Ragnaros, -1, 50
[Sulfuras, Hand of Ragnaros, 0, 0] => Sulfuras, Hand of Ragnaros, 0, 0
[Sulfuras, Hand of Ragnaros, 0, 1] => Sulfuras, Hand of Ragnaros, 0, 1
[Sulfuras, Hand of Ragnaros, 0, 49] => Sulfuras, Hand of Ragnaros, 0, 49
[Sulfuras, Hand of Ragnaros, 0, 50] => Sulfuras, Hand of Ragnaros, 0, 50
[Sulfuras, Hand of Ragnaros, 2, 0] => Sulfuras, Hand of Ragnaros, 2, 0
[Sulfuras, Hand of Ragnaros, 2, 1] => Sulfuras, Hand of Ragnaros, 2, 1
[Sulfuras, Hand of Ragnaros, 2, 49] => Sulfuras, Hand of Ragnaros, 2, 49
[Sulfuras, Hand of Ragnaros, 2, 50] => Sulfuras, Hand of Ragnaros, 2, 50
[Sulfuras, Hand of Ragnaros, 6, 0] => Sulfuras, Hand of Ragnaros, 6, 0
[Sulfuras, Hand of Ragnaros, 6, 1] => Sulfuras, Hand of Ragnaros, 6, 1
[Sulfuras, Hand of Ragnaros, 6, 49] => Sulfuras, Hand of Ragnaros, 6, 49
[Sulfuras, Hand of Ragnaros, 6, 50] => Sulfuras, Hand of Ragnaros, 6, 50
[Sulfuras, Hand of Ragnaros, 11, 0] => Sulfuras, Hand of Ragnaros, 11, 0
[Sulfuras, Hand of Ragnaros, 11, 1] => Sulfuras, Hand of Ragnaros, 11, 1
[Sulfuras, Hand of Ragnaros, 11, 49] => Sulfuras, Hand of Ragnaros, 11, 49
[Sulfuras, Hand of Ragnaros, 11, 50] => Sulfuras, Hand of Ragnaros, 11, 50

***

廣告

對於軟體測試以及軟體重構有興趣的鄉民,可以參考泰迪軟體以下兩個課程:

***

友藏內心獨白:終於可以開始重構了。

2019年2月19日 星期二

透過測試涵蓋率讓重構更有信心(中):突變測試

Feb. 18 23:47 ~ Feb. 19 00:39

▲Ada:切換為超人模式XD


突變測試

如果測試案例涵蓋足夠多的待測程式(System Under Test;SUT)行為,當待測程式被改變時,重新執行所有的測試案例其結果應該失敗。這代表測試案例捕捉到不正確的程式行為(與測試案例所記載的程式行為不符),突變測試(Mutation Testing)的想法就是從此而來—藉由改變待測程式的邏輯,驗證測試案例是否足夠。

***

例子

▼首先手動把withdraw程式的 amount > 0 改成 amount >1,然後重新執行測試案例。如果測試案例涵蓋率足夠多,其執行結果應該是失敗的(因為此時待測程式的行為被更改和原本測試案例所記錄的行為不同)。


▼執行結果居然是綠燈,原本三個測試案例都通過,也就代表原本的測試案例有遺漏了程式的行為,趕快補寫測試案例吧。


▼先把原本withdraw函數的程式碼復原成突變前的正常版本,然後增加一個單元測試來涵蓋剛剛程式碼突變後所沒捕捉到的行為。


▼重新執行全部四個單元測試案例,成功。


▼這時候在手動把withdraw程式的 amount > 0 改成 amount >1。


▼重新執行四個測試案例,執行結果失敗,代表程式的突變行為被測試案例抓到了。換句話說,透過突變測試技巧,進一步增強測試案例的涵蓋度

***


更多的突變

▼再看一次withdraw的程式碼,鄉民們可以試著把 amount > 0 改成 amount == 0、amount < 0,或是把 balance >= amount 改成「小於等於」或是「等於」,來驗證測試案例是否足夠。

以這個例子來看,上述各種突變狀況都可以被四個測試案例給找出來。這時候鄉民們就有相當高的信心,可以開始動手重構,因為「安全網」(足夠的測試案例)已經準備好了。

***


下集預告

withdraw函數的程式邏輯很簡單,就算沒有用突變測試技巧,也可以用傳統的邊界測試(boundary test)技巧來設計測試案例。

下一集將介紹特徵測試(Characterization Test)技巧與Approval Tests工具,針對程式邏輯複雜到爆掉且沒有人看得懂的函數,產生足夠多的測試案例來支援後續重構活動

***

廣告

對於軟體測試以及軟體重構有興趣的鄉民,可以參考泰迪軟體以下兩個課程:

***

友藏內心獨白:疫苗接踵次數要足夠才能有完整的抵抗力。

2019年2月18日 星期一

透過測試涵蓋率讓重構更有信心(上):分支涵蓋率

Feb. 18 21:49~22:50

▲很多開發環境都內建測試涵蓋率工具,上圖為Eclipse


如何定義程式的行為?

重構(refactoring)是在不改變軟體外在行為的前提下,改變內部結構以改善設計品質。但是問題來了,你怎麼知道你在重構的過程中沒有改變軟體的行為?這個問題,更廣義一點來說,就是你要如何定義軟體的行為?

學過程式語言的人都知道,程式語言有「語法」和「語意」。語法定義結構,語意定義行為。語法錯誤可藉由編譯器(Compiler)幫忙找出,所以開發人員並不會害怕語法錯誤。

但是,整個程式的行為(語意)是由撰寫程式的人所定義,一般來說編譯器是無從得知開發人員的意圖。在開發人員修改程式的過程中,如果不小心改壞掉,原本可以動的程式不能動了,或是出現未被查知的bug,那就很麻煩了。

正規程式語言定義語意的方法過於抽象,大部分的開發人員無法直接運用,退而求其次,實務上開發人員通常透過撰寫測試案例來描述或紀錄程式的行為

***

測試案例足夠嗎?

透過測試案例採用列舉(舉例子)的方式來描述程式行為,就衍生出另一個問題:你怎麼知道你的測試案例足夠多到可以把程式的(主要)行為都表達出來?在傳統軟體測試領域,這個問題叫做測試案例足夠性條件(test case adequacy criteria),常見的方式是透過測試涵蓋率來判別測試案例是否足夠。

今天不是要討論測試,而是要借用測試涵蓋率的觀念,幫助開發人員在重構前判斷測試案例是否足夠支持後續的重構行為。

***

例子

▼你有一個Account物件用來代表客戶的銀行帳戶,你想要重構withdraw函數,但是目前沒有任何測試案例。雖然withdraw程式碼只有短短幾行,但你還是有點害怕萬一重構了之後程式有問題怎麼辦。


▼因此在重構前你準備補寫測試案例,你很快地設計了以下兩個測試案例:


▼接著你觀察withdraw函數的測試涵蓋率,發現第19行分支(branch)呈現黃色,表示還沒被100%涵蓋。


▼於是你繼續增加一個新的測試案例。


▼此時withdraw函數的程式碼全部變成綠色,代表達到100%的分支涵蓋率。

***

下集預告

達到100%分支涵蓋率並不代表測試案例通過程式就沒有bug,換句話說並不表示程式的所有行為都已被測試案例所記錄下來。但這至少是一個在實務上可行且有一定代表意義的涵蓋率,可以當作預備開始重構前的第一個小目標。

下一集將介紹突變測試(Mutation Testing)技巧,可以進一步驗證測試案例的有效性。

***

廣告

對於軟體測試以及軟體重構有興趣的鄉民,可以參考泰迪軟體以下兩個課程:

***

友藏內心獨白:學習測試涵蓋率真的有用。

2019年2月13日 星期三

Scrum團隊如何落實自動化測試與持續整合?

Feb. 13 15:25~16:26


問題

朋友的Scrum團隊跑了半年,每個sprint結束所產生的可執行軟體離「潛在可釋出」還有一大段距離(品質不是很好)。團隊開始思考要如何提升品質,讓每個sprint可以交付「潛在可釋出的產品增量」。

團隊從QA部門借調一位測試人員—QA甲,希望借助他的力量提升軟體品質。但是QA甲以往都是用人工手動測試來驗證功能的正確性,並不會撰寫自動化測試案例。因此,QA甲需要等待團隊成員完成user story之後才可以開始測試。但通常user story完成後也接近sprint尾聲,沒剩多少時間讓QA甲去手動測試。

有團隊成員建議,請QA甲在下個sprint測試上個sprint的功能,但是這又引發出其他的問題,例如:

  • 程式碼凍結:QA甲要測試上個sprint的功能,就必須要求開發團隊不要修改上個sprint所完成的功能。實務上並不可行,因為下個sprint很有可能就是要基於上個sprint所完成的功能繼續開發,要求程式碼凍結會讓開發流程過於僵硬。
  • Done Done:因為上個sprint完成的功能留到下個sprint才驗收,那到底上個sprint做完的功能真的有做完嗎?換句花說,上個sprint的Done不是真的Done,還要經過下個sprint的驗證後才知道是不適真的Done。那如果下個sprint找出上個sprint的bug,又該在何時處理?如此一來,沒完沒了,整個開發時程也不容易掌握。

***

理想狀況

自動化測試與持續整合是軟體開發(不管是否採用敏捷)不可分割的一部分,團隊一開始在撰寫功能之前,就要先把持續整合環境架設好,讓自己的空專案可以在持續整合系統上建構,之後才逐一開發新功能。而每一個新功能,都要有足夠的自動化單元測試、整合測試與驗收測試。

為了達到測試自動化的目的,在Scrum團隊中的QA人員(Scrum團隊成員通稱為開發人員)需要具備撰寫自動化測試案例的能力。如果負責測試的開發人員沒有撰寫自動化測試案例的能力,就要考慮:

  • 是否願意學習新技能,並且與開發人員一起緊密合作
  • 考慮離開Scrum團隊

Teddy的意思並不是說所有的測試案例都必須要自動化,有些測試案例,例如UX方面的測試,或是自動化成本太高的UAT,或是探索式測試,還是可以用人工的方式來做。但大原則是絕大部分的測試必須要自動化,而身處Scrum團隊的測試人員也必須具備撰寫自動化測試案例的能力。

***

非理想狀況怎麼辦

如果團隊已經跑Scrum一陣子,一開始沒有落實自動化單元測試與持續整合,要怎麼「半路出家」?

假設團隊中沒有任何一個人具備自動化測試與持續整合個觀念,最快的方式就是請人教,或是去上課,把基本的知識在短時間先補齊。

接下來可以在DoD(Definition of Done)中訂定自動化驗收測試的標準,讓往後所開發的功能都有基本的自動化驗收測試。至於之前所開發的功能,可以採用以下兩種方式來補寫自動化測試:

  • Bug-driven:當發生bug的時候,先寫一個自動化測試案例來反映這個bug。此時這個測試案例一地會失敗,然後去修改程式碼,直到測試案例通過就知道bug改好了。
  • 預算制度:每個sprint投資固定時間去補寫之前的自動化測試案例。

***

測試先行或測試後行?

有些技術能力比較好的團隊,會採用測試先行(test first),也就是測試驅動開發(test-driven development)、行為驅動開發(behavior-driven development)、實例化規格(specification by example)的方式,讓撰寫測試案例成為開發流程中的上游,test code先於production code,以避免測試後行(test last)的缺點—先寫production code,永遠沒有時間寫test code。

Teddy覺得測試先行或是後行都可以,但重點是「測試一定要行」。只要有紀律,養成測試是開發不可分割的一部分,測試先行或測試後行可以依據團隊的能力與專案特性自行調整。

***

友藏內心獨白:自動化才能提高QA的價值。

2019年2月1日 星期五

繞過去,好嗎?

Jan. 31 23:07~23:58

▲逢山開路,遇水搭橋


問題

從去年開始Teddy和指導教授一起帶幾個實驗室學生做研究,目前遵循clean architecture的方法正在開發看板軟體。一個多禮拜前學生問Teddy關於Repository的介面設計,經過討論後Teddy請他們做點研究,下次開會再談。

今天回學校和學生開sprint review會議,順便請學生說明他們研究Repository的結果。學生告訴Teddy他們發現有兩種Repository的設計方法,第一種是接受由外部給定的specification當作查詢條件,可以比較有彈性的支援多種查詢條件。第二種是在Repository介面上設計常見的方法,例如findAll、findById、findChildrenById等。

學生採用第二種設計方法,聽了請他們選擇的理由後,Teddy還是覺得不滿意(forces沒有被平衡XD)。於是請學生把domain model叫出來,一起讀過一次,發現他們所採用的Repository介面設計無法支援domain model的所有物件。

***

程式可以動啊

學生寫的程式可以動,這個sprint所完成的user story還算做得不錯。但是,如果細看軟體設計,其實還有不少可精進之處。

Teddy告訴他們:

Repository的問題還沒徹底解決,雖然程式可以動,但是如果不管這個設計問題,繞過去,以後你們還是有很大的機會會再遇到它。你們是研究生,專案中遇到問題應該利用機會把問題搞懂,徹底解決。日後遇到同樣的問題,既使context不同,別人花三天,你們只要花一小時就可以解決。層次與專業就是這樣累積出來的。

這個問題,最好的解決方式就是你們搞懂後做出設計的決定,然後說服我(教我)。次一等的解決方式,就是我弄懂後教你們。最糟的方式就是不管它,反正程式可以動就好。

***

Repository Pattern

因為Google太方便,所以學生遇到問題尋找資料,大多下個查詢條件,然後看最前面的幾個連結之後就做出結論,草草結束。Teddy在〈增進學習力的三個練習〉提到,首先要知道名詞的定義。學生只知道可以套用Repository pattern,但很可能對於該pattern的定義只是一知半解。

在《Patterns of Enterprise Application Architecture》的第322頁就介紹Repository pattern。在《Domain-Driven Design: Tacking Complexity in the Heart of Software》以及《Implementing Domain-Driven Design》也都有提到,後者並且包含範例程式碼與實作細節。

***

龜毛

大家都說日本人做事很龜毛。同樣的商品,例如Toyota汽車,MIT和MIJ,相信大部分的人還是比較喜歡日本原裝進口。為什麼?因為組裝的人不一樣啊…Orz

軟體開發是一種專業,就好像醫生也是一種專業。你總不希望醫生開刀的時候遇到問題「繞過去」吧?!反正 程式可以動 人還有呼吸就好XD。 

***

友藏內心獨白:書還是要讀,不能只看網路文章。