程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 利用Ruby簡化你的Java測試(進階篇)

利用Ruby簡化你的Java測試(進階篇)

編輯:關於JAVA

本文是Productive Java with Ruby系列文章的第二篇,通過上一篇的介紹,我想大家對如何利用Ruby進行單元測試有了一個基本的了解,從這裡開始,我將和大家一起討論一些利用Ruby進行單元測試時的高級話題。

通常,新技術的引入只能降低解決問題的難度,而不是消除問題本身!

在“依賴”的原始叢林中掙扎...

通過Ruby我們可以更高效的處理數據准備的問題,但是真實的世界並不那麼簡單!隨著測試的深入,我們會越發的感覺一不小心就掙扎在“依賴”的原始叢林中!有時候似乎需要加入無數的jar包,初始化所有的組件,配置完一切的數據庫、服務器及網絡的關系,才能開始一小段簡單的測試。更痛苦的是這一切是如此的脆弱,僅僅是某人在數據庫中多加了一條數據或者更改了一部分環境配置,你苦心構建的所有測試就全部罷工了!多少次,你仰天長歎:“神啊!救救我吧...”。可神在那裡呢?

Mock

單元測試之所以有效,是因為我們遵從了快速反饋,小步快跑的原則!一次只測試一件事情!而大量依賴的解決工作明顯讓單元測試偏離的原本的目標,也讓人覺得不舒服。Mock技術就能讓我們有效擺脫在叢林中的噩夢。我們知道,在計算機的世界裡,同樣的輸入一定能得到對應的輸出,否則就是異常情況了。Mock技術本質上是通過攔截並替換指定方法的返回值擺脫對程序實現的依賴。對於1+1這樣的輸入條件進行計算,Mock技術直接攔截原方法,替換該計算方法的返回值為2,不關心這個算法到底是通過網絡得到的,還是通過本地計算得到的。這樣就和具體實現解藕了。

在對Java進行單元測試的時候,通常會對某個具體類或某個接口產生依賴,要解藕就需要能夠對具體類或接口進行Mock。幸好這些在JRuby中都非常的簡單,由於JtestR自動為我們引入了mocha這個Mock框架,讓我們可以更簡單的開始工作。先看一個針對HashMap的Mock測試吧:

map = mock(HashMap)      #=> mock java.util.HashMap類,如果是接口可以直接new出來,例如Map.new
map.expects(:size).returns(5) #=> 模擬並期望調用size方法時返回5
assert_equal 5, map.size    #=>斷言,和JUnit斷言非常相似

EasyMock是個流行的開源Java Mock測試框架,在它的官方網站的文檔中剛好有如何利用Mock進行測試的示例,為了方便說明,我將直接引用這個示例,並用JRuby實現基於Mock的測試。首先我們有一個接口:

//協作者接口,用以跟蹤協作文檔的相關狀態
public interface Collaborator {
   void documentAdded(String title); //當新增文檔時觸發
   void documentChanged(String title); //當文檔改變時觸發
   void documentRemoved(String title); //當文檔被刪除時觸發
   byte voteForRemoval(String title); //當文檔被共享,並進行刪除操作是,執行投票的動作
   byte[] voteForRemovals(String[] title); //同上,不過可以同時投票多個文檔
}

在這個示例中,還有一個ClassUnderTest類實現了管理協作文檔的相關邏輯,簡化示例代碼如下:

public class ClassUnderTest {
   // ...  
   public void addListener(Collaborator listener) {
     // 增加協作者
   }
   public void addDocument(String title, byte[] document) {
     // ...
   }
   public boolean removeDocument(String title) {
     // ...
   }
   public boolean removeDocuments(String[] titles) {
     // ...
   }
}

到這裡開始,我們就可以開始利用JRuby進行測試了。上一篇中我介紹了Ruby的測試框架,不過這次,我們學習一個新的測試框架dust,它可以讓你以更簡潔的方式書寫測試:

import "org.easymock.samples.ClassUnderTest"
import "org.easymock.samples.Collaborator"
unit_tests do
   cut = ClassUnderTest.new
   mock = Collaborator.new #=> mock一個接口只需直接new出來即可
   cut.addListener(mock)

#測試方法以test開始,後面跟一段具有描述性的字符串,然後在block中完成測試邏輯

test "001 remove none existing document" do
     cut.removeDocument("Does not exist")
   end
end

將上述代碼拷貝至src/test/ruby下,運行mvn test命令,OK,通過了相關測試。非常簡單吧!dust甚至讓我們不用聲明任何類就可以開始工作了,處處都體現著ruby簡單、高效的理念!

加速

跑過幾次單元測試後,大家一定會發現測試代碼是很容易書寫,但是跑測試的時間似乎有點長!難道JRuby的性能這麼差?其實整個測試過程中啟動JRuby花費了很多時間,JtestR框架也考慮的很周到,只需要啟動一個本地的測試服務器就可以大大加快測試執行的速度,在shell中執行mvn jtestr:server即可。再跑一次單元測試,速度大大增加了吧!

上面的代碼只測試了刪除一個不存在的文檔,邏輯太過簡單,不能說明任何問題,我們繼續後面的測試,新增一個文檔:

test "002 add document" do
     mock.expects(:documentAdded).with("New Document") #=> 我們期待documentAdded被執行,並且title的值為“New Document”
     cut.addDocument("New Document", [])
   end

運行測試,居然出錯了,TypeError: for method addDocument expected [java.lang.String, [B]; got: [java.lang.String,org.jruby.RubyArray,原來錯在cut.addDocument("New Document", [])的方法中我簡單傳入了[],這是一個Ruby數組對象,將這段代碼改成:

cut.addDocument("New Document", [].to_java(:byte))

重新運行測試,OK,全部通過。在JRuby中進行測試時調用Java對象的方法要注意將Ruby對象轉換成Java對象。我們對比一下JUnit的代碼

@Test
public void addDocument() {
   mock.documentAdded("New Document");
   replay(mock);
   classUnderTest.addDocument("New Document", new byte[0]);
   verify(mock);
}

Ruby代碼還是稍稍比Java代碼簡潔一些,雖然優勢不明顯。我們繼續完成後續的測試,增加並改變一個文檔:

test "003 add and change document" do
   mock.expects(:documentAdded).with("Document")
   #在ClassUnderTest實現邏輯中,後續增加的同名文檔屬於修改操作,所以documentChanged事件被觸發了三次
   mock.expects(:documentChanged).with("Document").times(3) #=> DSL here
   cut.addDocument("Document", [].to_java(:byte))
   cut.addDocument("Document", [].to_java(:byte))
   cut.addDocument("Document", [].to_java(:byte))
   cut.addDocument("Document", [].to_java(:byte))
end

運行測試,全部通過!請大家注意mock.expects(..).with(..).times(3)這行代碼,代碼本身似乎就在說我期望這個對象的XXX方法被調用,參數是xx,並且一共被調用了3次。書寫簡潔,閱讀也非常的語義化!這就是我們所說的DSL(Domain Specific Language),mocha就是Ruby在Mock測試方面的領域化語言!它支持的語義非常的豐富,包括:

at_least  at_least_once  at_most  at_most_once  in_sequence  never  once  raises  returns  then  times  when

等等。DSL的應用是Ruby的一大特點,它甚至能讓我們寫出連客戶都能很容易看懂的測試代碼。這在敏捷實踐中,與用戶討論接收測試時就顯得非常有用及必要!我們也同樣對比一下JUnit和EasyMock的實現:p

@Test
public void addAndChangeDocument() {
  mock.documentAdded("Document");
  mock.documentChanged("Document");
  expectLastCall().times(3);
  replay(mock);
   classUnderTest.addDocument("Document", new byte[0]);
   classUnderTest.addDocument("Document", new byte[0]);
   classUnderTest.addDocument("Document", new byte[0]);
   classUnderTest.addDocument("Document", new byte[0]);
   verify(mock);
}

EasyMock屬於非常正常的API調用,沒有太多DSL的概念,在這方面JMock相對來說要好一些,不過和Ruby相比,表達相同的語義,還是更繁瑣一些。我們繼續完成最後一段測試代碼,刪除及投票:

test "004 vote for removel" do
   mock.expects(:voteForRemoval).with("Document").returns(42)
   mock.expects(:documentRemoved).with("Document")
   assert_equal true, cut.removeDocument("Document")
end

看到這裡,細心的同學一定會發現有些奇怪,並沒有先增加一個Tilte是Document呀?是的,這個是Ruby的單元測試和Java機制不一樣的地方,JUnit中,每個方法是在線程中執行的,不保證被執行的先後順序,而Ruby的單元測試是簡單反射,按字母排序後執行的,所以只有一個上下文環境。我特意在每個方法的描述前加了個數字序列,以保證按這個數字的大小順序執行!

好了,到這裡,對利用Ruby進行Mock測試介紹基本完成!剩余的EasyMock的示例測試留給大家自己完成吧!

總結

引入Ruby進行Mock測試可以有效簡化單元測試時對各種環境的依賴,但是Mock也有Mock自己的問題,例如,它需要你對被測試類的內部細節有一定的了解,畢竟利用Mock技術進行測試屬於白盒測試。當被測試類的內部實現有所改變而外部接口未發生變化時,原本不該出錯的測試方法依舊有被打破的風險。還是回到開篇的那句話:通常,新技術的引入只能降低解決問題的難度,而不是消除問題本身!

作者介紹:殷安平,現任阿裡軟件研究院平台二部架構師,工作6年以來一直從事Java開發,愛好廣泛,長期關注敏捷開發。對動態語言有了強烈的興趣,致力於將動態語言帶入實際工作中!工作之余喜歡攝影和讀書。個人RSS聚合: http://friendfeed.com/yapex。聯系方式:anping.yin AT alibaba-inc.com。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved