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

利用Ruby簡化你的Java測試

編輯:關於JAVA

Martin Fowler:當然(願意花掉一半的時間來寫單元測試)!因為單元測試能夠使你更快地完成工作。無數次的實踐已經證明這一點。你的時間越是緊張,就越是要寫單元測試,它看上去慢,但實際上能夠幫助你更快、更舒服地達到目標。

單元測試很重要,但是……

單元測試的重要性,我想再多做一些強調也不為過。但實際情況是我經常聽到Java開發人員抱怨單元測試繁瑣、難寫。雖然勉強為之,卻疲於奔命,並沒有體會到它的好處!最終造成的結果是出現了大量只能運行一次的單元測試。是將責任簡單歸結於開發人員?還是開發流程或制度的不完善?

平心而論,我自己在做TDD或單元測試的時候,有很多時候也確實覺得無趣,尤其是在一些准備測試數據或測試環境的工作上,例如我們經常需要隨機生成特定長度的字符串用於測試,需要如下代碼:

public String getRandomAlphabetic(int count) {
       count = count <= 0 ? 5 : count; //默認為5
       //構建一個包含所有英文字母的字符串
       String alphabet="abcdefghijklmnopqistuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
       StringBuffer sb = new StringBuffer(count);
       for (int i = 0; i < count; i++) {
         int character=(int)(Math.random()*26);
         sb.append(alphabet.substring(character, character+1));
       }
       return sb.toString();
   }

如果用Ruby的話

def random_alphabetic(size=5)
       chars = ('a'..'z').to_a + ('A'..'Z').to_a # 構建一個從a到Z的一個字母表數組
      (0...size).collect { chars[rand(chars.length)] }.join # 從chars數組中返回指定長度的隨機字符數組(默認5個),調用join方法,將這個數組中的所有元素連接成一個字符串
   end

對比後大家感覺如何?有經驗的開發人員馬上會挑戰說,我們有現成的commons-lang庫,簡單調用RandomStringUtils.randomAlphabetic(5)就可以完成任務,可我想問的是,如果沒有第三方庫的支持,你更願意用哪種方式?還可以想象構建一個樹狀結構的數據,Ruby的方式

data =<<-EOF
   {
   "order_id": "xxx-xxxxx-xxx",
   "books": [
     {"ISBN": "2323-2323", "number": 2, "price": 20.00},
     {"ISBN": "2323-2324", "number": 3, "price": 30.00},
     {"ISBN": "2323-2325", "number": 2, "price": 20.00},
     {"ISBN": "2323-2326", "number": 3, "price": 30.00},
     {"ISBN": "2323-2327", "number": 2, "price": 20.00}
    ]
   }
   EOF     # 該數據為json格式的一段字符串
   order = JSON.parse(data)
   p order['books'][0]['ISBN'] #=> 2323-2323

用Java該怎樣完成,很多人會祭出Java世界中最被濫用的七種武器之首“xml”,即便如此能完成的如此優雅嗎?如果不是簡單的“語言宗教崇拜”,至少我會毫不猶豫的選擇用Ruby的方式完成任務。省點時間,早點下班陪陪老婆也好啊!:)

在Ruby的世界裡

那作為一個Java的開發人員,如何享受到Ruby在測試方面給我們帶來的好處呢?事實上Java早就為Ruby、Python等腳本語言做好了准備,JRuby是sun對Ruby on JVM的官方支持,現在的版本是1.1.3,已經能夠非常好的讓Ruby運行在Java的世界裡。

在開始之前,我想簡單介紹一下跟本文相關的Ruby及JRuby的基本用法,具體參考Ruby Home

# 這是一段注釋
   puts 'Hello, World !' #打印Hello, World!這個字符串,相當於Java中的System.out.println
   ('a'..'z') #聲明一個a到z的range類型的數據,range表示一個連續的范圍,當然也可以是一段連續的數字
   ('a'..'z').to_a #簡單調用方法to_a將一個range類型的數據轉換成一個數組
   <<-EOF
   ...
   EOF # Ruby通過配對EOF的方式聲明一個多行的字符串
   [1,2,3,4,5] #聲明一個數組
   [..].each {|it| puts it } #通過each方法遍歷每個元素,其中{...}表示一個代碼塊,在這裡的語義是在遍歷每個元素時打印這個元素,其中it是隱式聲明的參數,表示當前被遍歷到的元素
   對於數組可以用select, find, collect等方法遍歷,用<<, push, pop, delete等方法改變數組裡的元素
   h = {'id'=>'1', 'name'=>'foo', 'age'=>24} #簡單聲明一個Hash
   h['name'] = 'bar' #對key為name的條目賦值
   h['age'] #24
   class Foo < Base #通過class關鍵字聲明一個類,‘< Base’表示從基類Base繼承
     @name   #聲明一個實例變量
     @@count #聲明一個類變量,相當於Java中static關鍵字修飾的變量
   end

在JRuby中可以直接使用所有的Ruby類和方法,也能夠很輕松的調用Java的類庫,實際上JRuby將Ruby代碼動態編譯成JVM的字節碼,具體參考JRuby

require 'java'          #引入對Java的支持
   import 'java.util.ArrayList'  #導入需要的某個包
   list = ArrayList.new      #創建一個ArrayList
   [1,2,3,4,5].to_java      #將Ruby類型轉成對應的Java類型

從上面簡短的例子和基本介紹,我們能發現什麼?Ruby對數組,字符串等基本類型提供了強大的支持,而這些恰恰是Java缺乏的,我們沒有辦法簡單的創建一個數組,不能用簡單的方式遍歷這些集合,甚至都不能簡單聲明一個多行的字符串。而這些在進行測試工作,准備測試數據的時候都是必不可少的!利用Ruby的這些特性,我們可以極大的增加開發的效率,擺脫相當多繁瑣的工作。當然,這些只是Ruby為我們提供的諸多好處中最直觀的部分,隨著我們的討論深入,我們將看到越來越多有意思的特性。

准備工作

用Ruby進行測試,我們需要JtestR這個專門為簡化Java測試而准備的Ruby測試工具,當前的最新版本是0.3.1。如果你使用maven,在pom.xml中加入

<plugins>
     ...
     <plugin>
       <groupId>org.jtestr</groupId>
       <artifactId>jtestr</artifactId>
       <version>0.3.1</version>
       <configuration>
         <!-- Ruby測試文件所在目錄 -->
         <tests>src/test/ruby</tests>
       </configuration>
       <executions>
         <execution>
           <goals>
             <goal>test</goal>
           </goals>
         </execution>
       </executions>
     </plugin>
     ...
   </plugins>

使用ant的開發人員請參考這裡。用Ruby做單元測試和Java一樣,簡單從Test::Unit::TestCase繼承即可

class MyFirstJRubyTests < Test::Unit::TestCase
     def test_true
       assert true
     end
   end

可以將這個測試文件簡單拷貝到myProj/src/test/ruby目錄下,運行mvn test,你會看到JtestR產生的測試結果輸出

[INFO] [jtestr:test {execution: default}]
   Other TestUnit: 1 tests, 0 failures, 0 errors

在這段輸出報告之上,你應該還能看到正常的Java unit testcase輸出的測試結果,這表明,我們可以在開發的過程中同時選擇用Java的方式測試,或用Ruby的方式測試!

JRuby測試之旅

好了,一切准備好之後,就可以開始我們的Ruby測試之旅了!你一定不希望自己苦心經營的blog或論壇上出現某些“不和諧”的詞,尤其是在這舉國歡慶的特殊階段。你設計了一個專門用於過濾帶有這些關鍵服務接口

public interface KeywordFilterService {
     //過濾訪客評論字符串數組,返回一個新的不包含敏感關鍵字的結果
     String[] filter(String[] comments);
     //獲取被過濾的訪客評論
     String[] getFiltedComments();
   }

並寫了一個很簡單的實現類class KeywordFilterServiceImpl implements KeywordFilterService,這個類的實現我們就暫不關心,把重點聚集在如何對這個實現類進行測試上。首先在myProj/src/test/ruby目錄下新建test_keyword_filter_service.rb文件,鍵入以下內容

require 'test/unit'
   class KeywordFilterServiceTest < Test::Unit::TestCase
     def setup
       @keywords = %w{X XX XXX XXXX XXXXX XXXXXX XXXXXXX} #不用加引號,更方便
     end
     def test_filter

     end
   end

setup方法准備了我們要測試的關鍵字數據,在Ruby中%w{...}用來簡單定義字符串數組。test_xxx方法就是我們的測試方法。有了關鍵字數據後我們還需要一組用來測試的測試數據,裡面一部分包含我們的關鍵字。我決定用上面定義的隨機生成字符串的方式產生這些測試數據

def random_alphabetic(size=5)
       chars = ('a'..'z').to_a + ('A'..'Z').to_a
       (0...size).collect { chars[rand(chars.length)] }.join
     end

     def random_comments
       comments ||= []
       10.times do
         keyword = rand(10) % 3 == 0 ? ' ' : @keywords[rand(@keywords.length)] #隨機決定是否包含關鍵字
         comment = random_alphabetic + keyword + random_alphabetic
         comments << comment
       end
       return comments
     end

這樣,每次產生10條數據,有近三分之一的數據中包含不和諧的關鍵字。有了測試數據剩下的工作就很簡單了,我們只需調用寫好的Java服務,對返回的測試數據進行驗證即可,由於需要調用Java服務,和Java一樣,我們首先要引入類:

import 'com.alisoft.research.JRuby.service.KeywordFilterServiceImpl'

測試方法實現如下:

def test_filter
       comments = random_comments
       service = KeywordFilterServiceImpl.new(@keywords.to_java :String)
       filted = service.filter(comments.to_java :String)
       forbiddens = service.getFiltedComments

       assert forbiddens.length == comments.length - filted.length
       assert_equal forbiddens.sort, (comments - filted).sort
     end

其中,有兩點需要注意,首先,我們可以通過to_java方法將Ruby類型轉換成Java類型,例如上面將@keywords.to_java :String表明將Ruby數組轉換成Java的String數組。第二,Ruby對數組支持“-”的操作,表示將一個數組減去和另一個數組中相同的元素,非常的直觀!很明顯,被過濾的數組應該等於原來的數組減去過濾後的結果!運行mvn test,我們將看到

[INFO] [jtestr:test {execution: default}]
   Other TestUnit: 2 tests, 0 failures, 0 errors
結論

說明新增加的測試通過!最後我們來對比一下實現同樣的功能Ruby和Java的差別

Ruby:
   require 'test/unit'
   import 'com.alisoft.research.JRuby.service.KeywordFilterServiceImpl'
   class KeywordFilterServiceTest < Test::Unit::TestCase
     def setup
       @keywords = %w{X XX XXX XXXX XXXXX XXXXXX XXXXXXX}
     end

     def test_filter
       comments = random_comments
       service = KeywordFilterServiceImpl.new(@keywords.to_java :String)
       filted = service.filter(comments.to_java :String)
       forbiddens = service.getFiltedComments

       assert forbiddens.length == comments.length - filted.length
       assert_equal forbiddens.sort, (comments - filted).sort
     end

     def random_alphabetic(size=5)
       chars = ('a'..'z').to_a + ('A'..'Z').to_a
       (0...size).collect { chars[rand(chars.length)] }.join
     end

     def random_comments
       comments ||= []
       10.times do
         keyword = rand(10) % 3 == 0 ? ' ' : @keywords[rand(@keywords.length)]
         comment = random_alphabetic + keyword + random_alphabetic
         comments << comment
       end
       return comments
     end
   end
   Java:
   package com.alisoft.research.JRuby.test;
   import static org.junit.Assert.assertArrayEquals;
   import static org.junit.Assert.assertEquals;
   import Java.util.ArrayList;
   import Java.util.Arrays;
   import Java.util.List;
   import org.apache.commons.lang.RandomStringUtils;
   import org.apache.commons.lang.math.RandomUtils;
   import org.junit.Test;
   import com.alisoft.research.JRuby.service.KeywordFilterServiceImpl;
   public class KeywordFilterServiceTest {
     @Test
     public void testFilteredResults() {
       String[] comments = getRandomComments();
       KeywordFilterServiceImpl service = new KeywordFilterServiceImpl(
           getKeywords());
       String[] filted = service.filter(comments);
       String[] forbiddens = service.getFiltedComments();
       assertEquals(filted.length + forbiddens.length, comments.length);
       assertArrayEquals(forbiddens, sub(comments, filted));
     }
     //實現減法操作
     private String[] sub(String[] all, String[] part) {
       List allList = new ArrayList(Arrays.asList(all));
       allList.removeAll(Arrays.asList(part));
       return allList.toArray(new String[allList.size()]);
     }
     private String[] getRandomComments() {
       String[] comments = new String[RandomUtils.nextInt(10)];
       for (int i = 0; i < comments.length; i++) {
         String comment = RandomStringUtils.randomAlphabetic(5);
         String keyword = RandomUtils.nextBoolean() ? getKeywords()[RandomUtils
             .nextInt(getKeywords().length)]
             : "";
         comment += keyword + RandomStringUtils.randomAlphabetic(5);
         comments[i] = comment;
       }
       return comments;
     }
     private String[] getKeywords() {
       String[] keywords = new String[] { "X", "XX", "XXX", "XXXX",
           "XXXXX", "XXXXXX", "XXXXXXX" };
       return keywords;
     }
   }

在借助了apache-commons-lang之後,LOC: Java 58, Ruby 35。大家也可以注意一下Java中實現兩個數組“減法”的代碼對比Ruby的實現,Ruby明顯更為直觀,更有效率!

利用Ruby對Java進行測試的基礎介紹就到這裡,希望能拋磚引玉,引起大家的興趣。下一篇我將和大家再討論一些例如mock等更高級的測試話題。

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

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