1. 程式人生 > >C++單元測試二:何時Mock及其是與非

C++單元測試二:何時Mock及其是與非

什麼時候需要mock

在前面一部分(C++單元測試一:並非看上去那麼簡單——幾個很實際的問題),我遇到的問題是:一個單元測試工程只能測一個被測類(其實前文的後記部分也已指出,其實建立新工程也不是特別必要,可以讓Mock類從被測類繼承,但問題是這是真的Mock嗎?);

那麼,是不是一個測試工程只能測一個類呢?還是這種方式本身就有問題呢?由於沒有找到實際工程中測試程式碼、被測程式碼的檔案組織方面的資料,我在Google Code上下載了幾個都帶有完整Unit Test的專案下來,看看他們是怎麼做UT的。從那幾個工程來看,他們的方式有如下兩種:

  • 建立多個小的測試工程;這種方式和我前面的這種方式一樣,整個單元測試包含多個測試工程,每個工程測試一個被測類,每個工程編譯成test_XXX.exe,然後有一個Runner會在某個目錄搜尋,並執行所有test開頭的exe,從而完成測試;但是這些工程裡面也很少看到Mock程式碼。
  • 整個單元測試程式碼,放置在一個工程裡面,將被測程式碼也引入測試工程一起編譯;如mango,同樣,它的測試也只看到幾個Mock而已,和整個被測的程式碼比較起來,只能算是冰山一角。

看了這些Google Code上的工程的單元測試,我不僅問自己,我真的需要為每一個被測類所依賴的物件都建立Mock嗎?帶著這個問題,找到了stackoverflow上的這篇討論:

這個帖子問的就是“我何時該Mock”。有人回答說要為每一個所依賴的物件都進行Mock,否則你做的不是單元測試,而是整合測試;也有人說這種方式太過激進和理想化。

我想首先我們要理一下,單元測試所說的“單元”到底是指什麼?一個方法?一個類?還是一個DLL/LIB?一個包?關於這個“單元”之所指,不同的資料有不同的說法,甚至這些說法相互衝突;維基百科給出的說法是:在過程語言裡面,這個單元可以是一個完整的模組,但通常被認為是一個方法(Function or Procedure);在面向物件的語言裡面,這個單元通常指一個介面,如一個類,也可能是一個方法;

好吧,我承認,我比較傾向於一個單元就是一個類這個概念,因此,上面那個帖子中的回答:如果不對類所依賴的物件進行Mock,那就是整合測試的說法,我也必須接受;但問題是,我們是否真的要為每一個依賴都進行Mock嗎?這篇文章(http://media.pragprog.com/articles/may_02_mock.pdf)列出了我們何時應該Mock的幾種場景:

  1.  The real object has nondeterministic behaviour. (真實物件有著不確定的行為) 
  2.  The real object is difficult to set up. (真實物件很難建立)  
  3.  The real object has behaviour that is hard totrigger (for example, a network error). (真實物件的行為很難觸發)
      
  4.  The real object is slow. (真實物件響應遲緩)  
  5.  The real object has (or is) a user interface. (真實物件是使用者介面)  
  6.  The test needs to ask the real object about howit was used (for example, a test might need to check to see that a callbackfunction was actually called). (真實物件使用了回撥機制)  
  7.  The realobject does not yet exist(真實物件尚不存在) 

其實,歸結起來,其深層的含義應該是:當你真正想把被測程式碼隔離開了的時候,才進行Mock。

好吧,我們回過頭來,看看我們在測ChatRoom的時候,是否真的需要為Message做Mock,針對上述的Checklist,我們的回答是:

  1.  No,Message的行為很確定;
  2.  No,Message很容易建立;
  3.  No,其行為很容易模擬
  4.  No,Message沒什麼特別耗時的操作;
  5.  No,它才不是UI呢
  6.  No,沒有任何回撥;
  7.  No,當然已經存在了,否則哪來的衝突。

Ok,看過上面的回答,我確信,我也不太需要為Message做一個Mock了。於是整個世界乾淨了,我可以在一個工程裡面做很多事兒了。

Mock致介面氾濫

好吧,我們退一步,假設真的要為Message類做Mock,該怎麼做呢?

方法之一是就是從Message繼承一個MockMessage出來,重新定義感興趣的方法;這個“Mock Object Patterns"中提到的self shunt多少有點類似。

方法之二就是某些人常說的“對介面程式設計”(個人感覺這個概念已經被妖魔化了,好像啥子事兒經介面轉一到手,就不是事兒了),於是我要為Message定義個介面IMessage(和蘋果沒啥子關係哈)出來,讓ChatRoom依賴於IMessage;那麼建立Message的Mock類並注入到ChatRoom中就很簡單了。可是,這裡面有幾個問題:

  1.   Message類本身並沒有打算做擴充套件,強制定義虛擬函式純屬冤枉;
  2.   Message本身還要存放資料,鑑於“一般不要在介面中放置資料成員”,讓IMessage情何以堪;
  3.  假設真要為每一個依賴物件,都做Mock處理;而要做Mock,設計時就要定義很多契約,也即介面,這會否導致介面滿天飛?介面氾濫?這篇文章(http://www.oracle.com/technetwork/articles/entarch/mock-shortcomings2-096727.html)就指出Mock的缺點之一就是會導致介面氾濫,然而卻冠以“對介面程式設計,而非對實現程式設計”的高帽子,這純屬對“對介面程式設計”的誤解;原文如下:

Mocks may lead to interface overuse

A possibleside effect of mock abuse is the unnecessary creation of Java interfaces, for the sole purpose of mock creation (trying to avoid the problems related to class-based mocks.) Typical examples include creation of interfaces that will have one and only one implementation, such as utility or helper classes.This practice is often justified by a misinterpretation of the principle " Program to an interface, not an implementation." This principle refers to the concept of interface, a supertype used to exploit polymorphism, not the Java construct interface. It is possible to program to an interface, implemented using a Java interface or an abstract class.

Creating interfaces to aidmock testing increases maintenance costs (because there is more code to maintain),which usually outweighs any benefit that mocks may bring.

Mock的是與非

必須承認,Mock是一個很有效的方法,他可以將你的測試和外部世界隔離開來,將問題圈在一個很小的範圍內,從而幫助你發現潛在的問題;此外,他也可以幫助我們思考如何做設計,例如當所寫的程式碼的可測試性(在不同粒度上的可測試性,單元測試、整合測試等)不佳,那也許就要考慮一下你的設計是否有問題;

然而,Mock不是測試的萬能鑰匙,他也只是服務於單元測試的一種技術,更何況單元測試,還只是測試工作中的一個很小的部分;

Mock有力量,Mock要慎用;否則,單元測試的可觀投入,不一定會有可觀的產出。