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