不要把Mock當作你的設計利器

來自:ThoughtWorks   李曉

前言

我不是個反Mock者,Mock有它的優勢,但使用它也同時帶來風險,我認爲使用Mock的基本原則是:不用。

不使用Mock,依賴一個設計簡單、職責清晰的代碼環境,因爲只有簡單的代碼才能和Mock的主要優勢相媲美,而使用這樣的代碼則可以避 免Mock所帶來的麻煩以及風險,從而達到不用Mock並改進你的測試和代碼設計的目的。

這裏面最重要的,就是不要讓你自己掉入Mock的陷阱,不要以爲Mock就是最佳的解決方案,使用Mock和其所帶來的設計複雜度以及Mock行爲依賴風險是需要做權衡的。

TurtleMock 是最近我和一些同事一起在做的一個開源mock framework。使用TurtleMock所mock出來的Object,默認就是一個簡單的NullObject實現,所有的方法都可以調用,返回 值按照java的類型默認值設計,只在你需要的 時候纔去assert Mock Object做了什麼。正是做TurtleMock促使我去思考爲什麼需要使用Mock,爲什麼我會更喜歡TurtleMock這種形式的Mock工具。 追根究底是因 爲現有的Mock工具使用白盒測試的方式輔助測試,而這種Mock方式的大量使用影響到了我對實現的設計和重構。進一步的思考讓我覺的目前在TDD過程中 對Mock的依賴性是不可取 的。

TDD (Test-Driven Development)不是Unit Test。相信無數人都提過這個話題,還是要在這裏帶一筆,因爲我熟悉的是TDD,所有有關Unit Test的知識幾乎都是來自於學習TDD的過程,所以我在下面的討論都是基於TDD的,Unit Test就不討論了,雖然我覺得也能成立。

文章中所有提及的“接口”一詞,都是指Java中的Interface,所有基於代碼的討論,都是基於Java的,畢竟不同語言太多不同特性,難以一一陳述。

文章中所提及的“TestCase”通常指的是一個Test Class中的一個Test Method。

文章中所提及的“Domain”是一種泛指,它可以是包含你的所有業務邏輯的Domain Model,也可以是包含顯示邏輯的Presentation;也可以把它簡單地分類爲除了UI、外部資源和第三方API之外的,我們自己設計實現幷包含邏輯的代碼。

你患有Mock依賴症嗎?

你在TDD過程中,有多少測試是不用Mock的?
你的TestCase的set up是不是比TestCase本身複雜得多?
是不是滿屏幕都是mock is expected to do something?
一旦進行重構,測試是不是因爲Mock的強制約定而失敗了一堆?
一旦重構代碼,是不是到處需要修改你的測試所依賴的Mock?
一開始寫測試,是不是就在想:我該Mock誰?
一碰到難以測試的情況,是不是腦袋裏就轉着“我有Mock我怕誰”?
出門見着朋友,是不是張口就是:你今天Mock了嗎:p

在你明確了自己的陣營之後,下面我們會討論下,這樣的依賴爲什麼是不可取的。

Mock的優點

Mock Object的行爲簡單,簡單到唯一,在set up好返回值後總是返回這個唯一值。

Mock Object的行爲可以預期,調用到你不希望調用的方法會讓測試失敗,方法被調用了你還可以驗證其參數。(TurtleMock和EasyMock可以生成一個簡單的NullObject的Mock Object,可以忽略對方法的非預期調用)

可以Mock一些在真實環境下難以模擬或出現的錯誤或異常。

Mock 是一種白盒測試工具(TurtleMock在你不去assert Mock Object的行爲時不是),傳統的Mock Object的set up過程就是目標代碼實現細節的設計過程(TurtleMock的set up 過程是你構造Mock Object行爲的過程,它並不關心目標代碼的設計實現)。(這個其實很難說是優點,它所帶來的問題也是很明顯的,見Mock的弊端2)

接口爲使用者設計,所以接口還未實現。Mock可以讓你以簡單的方式驗證使用者是如何使用接口的。

Mock的弊端

Mock Object的行爲依賴風險。Mock Object的行爲和真實對象的行爲必須一致,這在你對真實對象進行重構的時候是很大的風險。即使當前Mock Object的行爲和真實Object的行爲完全一直,而且所有測試都覆蓋到了,其結果也很可能是:代碼一處修改,測試到處失敗。實際開發過程中這種情況 是非常 常見的,而且已經有不少人依賴這一點來修改代碼了。其方法是先修改代碼讓所有依賴這塊代碼的測試都失敗,然後再一點一點修改測試代碼讓測試通過,這看起來 還非常不錯。

Mock Object的set up過程過於繁重。爲此,大多數Mock Object的set up過程都會在TestCase的環境set up過程中進行,由於Mock Object的set up直接導致如何對該Mock Object進行verify,這使得你的TestCase的set up過程實際上也在進行測試,測試的內容不但多而且難以分割成小的TestCase。從另一個角度說,過於複雜的Mock Object 的set up過程,也許說明你的代碼承擔的職責過多,分出更多小的職責清晰的類也許可以讓你避免這樣的情況出現。在實際應用中,太多的情況是,在一個 TestCase中,set up Mock Object的代碼比其它的代碼多得多。也許正是使用Mock勉強能夠測試你當前的設計,讓你止步不前;而一旦TestCase建立完整,過多的Mock 驗證又讓你的重構寸步難行;最終導致你一閉眼一蹬腿,忍了。

Mock Object 的set up過程通常難以閱讀。由於Java在jdk1.5之前沒有泛型支持,所以各種Mock工具的API都顯得不盡人意。其中JMock提供的一套API是比 較受好評的,因爲它使用起來簡單明確,接近自然語言的使用習慣。但是即使有再好的API支持,當一個TestCase 需要多個Mock Object協助時,仍然會顯得混亂而難以閱讀,因爲一個set up Mock Object的語句至少存在兩個含義:
Mock Object的行爲定義:
調用方法的返回值
調用方式時throw Exception
給調用方法時所傳遞的參數發送消息
TestCase期望assert的內容:
方法是否被調用以及調用的次數
調用方法時的參數是否合法
方法調用的順序

Mock工具的存在還助長了一種壞味道,就是你的 接口很可能會迅速膨脹從而承擔過多職責。傳統的Mock工具在生成一個接口的Mock後,所有不希望調用的方法在被調用時是會導致測試失敗的,所以你就會 忽略這個接口的膨脹,因爲看起來一切還都在你的掌握之中,從而導致它承擔過 多職責。這樣的接口其典型的特徵是一些Class使用這個接口的某一部分方法,另一些Class則使用其另一部分。這樣的接口如果使用Self Shunt模式進行測試,你會發現,真的是太髒了。

接口存在的目的

一個類實現
對應一個接口
爲什麼需要接口
爲了方便其它依賴這個類的類的測試
爲什麼依賴這個類的類的測試需要使用接口
因爲可以或者只能使用Mock

太多接口存在的唯一目的就是爲了測試,不是因爲別的,就是因爲容易Mock。這種設計複雜度的增加爲測試提供了很大的幫助,沒有它,是不是都沒辦法測試了?或者說,基本上已經是金科玉律了?在TDD大行其道的今天,可測試性高於一切的聖旨是不是太好用了?

接口有太多理由存在了,甚至有人提出面對接口編程,雖然那是對接口一詞的片面理解,但是,爲了方便Mock而從一個類抽取其所有public方法爲一個 接口 的做法,真的應該嗎?我實在厭倦了這種不得已的選擇,被Mock套上了枷鎖,矇蔽了雙眼,直至今天才重新審視,原來自己需要的是鼓足勇氣去重構。扔掉 Mock再披荊斬棘,似乎有點破釜沉舟的味道,但也不是什麼上青天的難事。

你的代碼爲測試做好準備了嗎?

似乎非常顯然,TDD的產物,難道還沒有爲測試做好準備?那麼:你的Object容易創建嗎?如果你的Object難以創建,你需要Mock。

你創建的Object行爲容易預測嗎?或者說,它的行爲邏輯複雜嗎?如果你創建的Object行爲分支過多邏輯複雜,你需要Mock。

你創建的Object職責是不是隻有一樣?如果你創建的Object職責很多,而你當前要進行測試的目標class只會牽涉其部分職責,你需要Mock。

越是簡單的東西,越是容易被測試,也越容易被用於測試,沒有複雜的分支,就不需要你去考慮這樣的情況怎麼樣,那樣的輸入數據又會怎樣。

Mock的最大優勢是什麼?就是它的行爲固定,它確保當你訪問該Mock的某個方法時總是能夠獲得一個沒有任何邏輯的直接就返回的結果。所以一個容易創 建行爲固定的簡單 Object是很容易在測試中使用的。相信馬上有人會認爲這樣的簡單是難以達到的,因爲總是有難以預料的複雜存在,以至於你直接就告訴我,那是不可能的。 在這裏我難以一一列舉每種情況以及每種情況的對策,一個簡單的原則是,使用多態解決多分支,使用更多的小類小單元替代大的複雜的類。大小的衡量標準?你的 測試。這需要你深入挖掘你的Domain,把所有單元分到足夠小,有時候你的一個複雜類的產生,純粹是由於概念上難以細分,實現一個由模糊的概念衍生的 Domain Object,往往會導致該對象的複雜度增加。當然,現實是很多Domain Object沒有真正去做之前,只能有個模糊的概念,手裏的需求往往是功能級的描述,設計實現正是你要做的事情,如果你不能一眼看穿其本質,那麼實現的過 程中就總是會有意外的驚喜出現,沒有關係,你有至少兩個選擇:

做Spike,無論如何,先證明你的想法是能走通的,通過 Spike把細節挖掘清楚,然後勇敢地把所有Spike代碼刪掉,一點一點地通過TDD重新實現。對於TDD的初學者我認爲這個是非常必要的,因爲我覺得 做TDD,很大一部分比拼的就是對細節的挖掘能力。猶如庖丁解牛,對細節瞭如指掌,自然能遊刃有餘。

直接TDD去實現,仔細分析並 建立 完整的to do list,每一步都要有勇氣去做放棄或者規模較大的重構,讓實現慢慢清晰起來,大膽地分離職責,而不是任由目標class的職責膨脹,僅通過不斷加 test case來完成所有的需求。這需要時刻把握目標class的唯一職責。很多人在學TDD的時候總是會問,到底TDD的一個Step多大合適?我覺得對於任 何人來說,越小越合適,大的Step是很誘人且看起來是很容易做到的(太難做好),小的Step則看起來讓人有些無從下手但找到下手的地方後就容易了。而 能讓你選擇大的Step的唯一理由是,你的腦子轉得夠快,也就是說,在你的大腦中已經 完成了所有的小Step,對你來說,一個稍大點的Step已經是顯然的了。通常這種情況我認爲在重構的過程中非常多,實際在TDD中,反而有些不需要,因 爲如果你能做大的Step,那麼小的Step對你來說,只是不去動腦筋罷了,省力很多:) 當然,仍然很多人認爲去動腦子比動手省力,我每次有這樣的念頭時都會被難以通過的test case鬱悶到:(

迴歸Mock

Mock我們仍然是需要的,在我們遇到如下問題時,Mock是我們的第一選擇:

外部資源,比如文件系統(Java的文件系統接口少,難以Mock,不過現在已經有不少開源項目專門做了內存的文件系統用於測試,比如cotta),這是因爲對此類外部資源依賴性非常強,而其行爲的不可預測性很可能導致測試的隨機失敗。

UI,這個實際上和外部資源也搭得上邊,因爲UI很多時候需要用戶行爲觸發事件。MVC和MVP模式都很好地解決了這個問題。

第三方API

當接口屬於使用者,通過mock該接口來確定測試使用者與接口的交互,明確定義該接口的職責。

在處理這些問題的過程中,特別是面對外部資源和第三方API時,Mock的風險是比較大的,多做Spike,爲對應行爲建立一組Acceptance測試是一個好的選擇。

顯然在你建立的Domain內部,你不應該去想着用Mock,不應該去想該不該 用Mock,念頭也不要動。你可以通過使用Observer去隔離對UI的依賴,通過Proxy去隔離對數據持久層的依賴,通過Adapter、 Proxy或者Stairway to heaven模式去隔離對第三方API的依賴,簡單地說,Domain用到什麼難以測試的外部包,使用接口隔離,把接口留在Domain裏讓依賴倒置,讓 其它API去依賴Domain,提高你的Domain的獨立性和可測試性,讓你的代碼真正爲測試做足準備,從而在Domain裏脫離Mock的苦海。

相信很少有人真正有心去讀Kent Beck的《TDD》一書中Part 1 — Chapter 17 Money Retrospective中的Code Metrics,這個表格中的第四行:Cyclomatic complexity ([3])?1.04?1

理解這裏面的1.04和1所代表的意義,你也就理解我現在的感慨。也許有人認爲Money這個例子太理想化了,但是又有多少人能夠在完成Money這個 例子 時能夠達到這樣的標準;曾幾何時,TDD也是那麼遙不可及。無論如何,如果使用Mock增加了你的測試代碼的複雜度,想想我今天的話:)

References
Kent Beck. Test-Driven Development By Example. Reading, Mass.: Addison-Wesley, 2002.
Robert C. Martin. Agile Software Development, Principles, Patterns, and Practices. Reading, Mass.: Prentice Hall, 2002.
Introduction to Test Driven Development(TDD)
更多的TDD相關資料可以從這裏找到:http://www.testdriven.com/
Tim Mackinnon, Steve Freeman and Philip Craig pioneered the concept of Mock Objects, and coined the term. They presented it at the XP2000 conference in their paper Endo Testing: Unit Testing with Mock Objects
JMock
EasyMock
TurtleMock
這裏可以找到有關極限編程(XP)的詳細介紹:http://www.extremeprogramming.org/


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章