JUnit 單元測試的基礎
我可以大言不慚地說(shuō),JUnit本身是一個(gè)相當簡(jiǎn)單的框架,想必各位也對其做過(guò)一些研究,所以直接給出一個(gè)自己寫(xiě)的簡(jiǎn)單的例子,來(lái)說(shuō)明JUnit的特點(diǎn)。
//Money.java:
public class Money {
private int amount;
public Money(int i){
amount = i;
}
public int amount(){
return amount;
}
public Money addMoney(Money mon){
return new Money(mon.amount()+this.amount());
}
//The key point here
public boolean equals(Object o){
if(o instanceof Money)
return ((Money)o).amount() == this.amount();
else
return false;
}
}
//TestMoney.java
import junit.framework.*;
public class TestMoney extends TestCase{
Money m11;
Money m12;
Money m13;
protected void setUp(){
m11 = new Money(11);
m12 = new Money(12);
m13 = new Money(13);
}
public void testEquals(){
Assert.assertEquals(m11,new Money(11));
Assert.assertEquals(m11,new Money(11));
}
public void testAdd(){
Money m23 = new Money(23);
Assert.assertEquals(m11.addMoney(m12),m23);
Assert.assertEquals(m11.addMoney(m12),m11);
}
public static void main(String args[]) {
junit.textui.TestRunner.run(TestMoney.class);
}
}
看到這里你可能會(huì )罵我把小孩子都能寫(xiě)出的東西貼到這里來(lái)丟人,其實(shí)就是這么個(gè)簡(jiǎn)單的例子足以說(shuō)明JUnit的運作原理。class Money重載了方法equals(), 就是為了進(jìn)行Money對象之間的比較。這樣的比較在JUnit中是通過(guò)斷言的方式進(jìn)行的,由于基礎類(lèi)TestCase繼承于Assert類(lèi),從而繼承了Assert類(lèi)提供的所有斷言方法。所以一句話(huà)概括,JUnit是通過(guò)斷言機制進(jìn)行底層對象間的比較來(lái)判斷功能正確與否的。你可能會(huì )抬杠的說(shuō):“不僅是對象吧,JUnit也可以比較兩個(gè)int或者其他的primitive data??!”,但在OO的理論中,Java中的primitive data也應該是對象(如SmallTalk中的實(shí)現),但Java出于對性能的考慮,對primitive data沒(méi)有采取類(lèi)的實(shí)現方式,但同時(shí)也給出了各個(gè)primitive data 的wrapper class。
最初認識到JUnit的這樣的工作原理,我有些失望懷疑它能否勝任復雜的商業(yè)邏輯的測試,看到了Fund Connect的performance test中測試service部分的代碼,我這樣的疑慮被消除了。下面節選一段代碼說(shuō)明:
public class AdminServiceTest extends TestCase
{
private static Log log = LogFactory.getLog(AdminServiceTest.class);
private static ServiceFactory factory;
protected static AdminService ds;
private static ServiceConfigurator serviceConfig;
private Statement stmt;
private ResultSet rs;
private String sConnStr;
private String sqlStr;
private Connection conn=null;
/**//**
* Constructor for AdminServiceImplTest.
* @param arg0
*/
public AdminServiceTest(String arg0)
{
super(arg0);
}
protected void setUp() throws Exception
{
//……
}
protected void teardown() throws Exception
{
// …..
}
public void testGetFundProcessors()
{
sqlStr = "select distinct Fund_Processor_uid from FUND_PROCESSOR_INSTR_XREF ";
try {
rs = stmt.executeQuery(sqlStr);
List result = (List)ds.getFundProcessors();
int i = 0;
while(rs.next()){
i ++;
}
assertEquals(i, result.size());
} catch (SQLException e) {
e.printStackTrace();
} catch (AdminServiceException e) {
e.printStackTrace();
}
}
//……
}
從這個(gè)例子中可以看到,無(wú)論是多么復雜的邏輯(testGetFundProcessors)最終都能轉化成底層對象通過(guò)斷言的比較(紅色字體部分)。“Everything is object. ”, JUnit的工作原理決定了它應該是單元測試的基礎。
另外,我也看了一下JUnit的源碼,代碼并不是很多多,由于應用了一些模式,使其結構設計較好。例如:TestCase類(lèi)中,在設計run()方法的繼承問(wèn)題時(shí),應用了Template Method Pattern; 對于多個(gè)test方法,要有針對性地生成相應的TestCase,應用了Adapter Pattern;等等。大家有興趣的,可以對其源碼進(jìn)行研究。
強大的測試工具JMeter
我看過(guò)的所有的Apache的項目,都很成功。JMeter也不例外,說(shuō)其強大,我個(gè)人認為有以下三個(gè)原因:
1、 較為友好的圖形用戶(hù)界面,易于測試人員使用,只要明白其中的原理用JMeter作測試是件愉快的事情而且它能夠方便的生成測試腳本。
2、 ThreadGroup概念的引進(jìn),這個(gè)概念在JMeter是相當重要的,之所以JMeter能夠完成對各種不同服務(wù)器的壓力測試與性能測試,也仰仗著(zhù)ThreadGroup。在一個(gè)ThreadGoup中可以規定應用的線(xiàn)程數量(Number of Threads每一個(gè)線(xiàn)程代表一個(gè)用戶(hù)),也可以規定用戶(hù)行為的重復次數(loop count)。在企業(yè)級應用的測試中,模擬多個(gè)用戶(hù)同時(shí)執行操作或同時(shí)處理數據的操作,利用ThreadGroup可以輕松實(shí)現。
3、 JMeter將大量的Test Target作了非常好的封裝,用戶(hù)可以直接使用這些封裝好的部件,大大減少了測試的工作量。比如測試WebService用的WebService SOAP Request, 測試網(wǎng)頁(yè)的HttpRequest,測試數據庫連接的JDBC Request等等。測試工作進(jìn)而簡(jiǎn)化成了選擇組建,將各個(gè)組建有邏輯的組織在一起的過(guò)程。
對于JMeter的強大以及其應用方法,因為大家都懂,所以我在這里不多說(shuō)了。下面談?wù)剛€(gè)人認為JMeter的不足之處。為了更清楚的說(shuō)明這個(gè)問(wèn)題,我將結合Fund Connect項目中,對于Server Performance Test的一些JMeter腳本進(jìn)行闡述。其中的一個(gè)Requirement如下:
Investors submit trades via GUI
Description: 15 investors log into FundConnect via GUI. Each investor submits 20 orders via a file upload.
Precondition: 20 investor institutions and 20 investors (one investor per investor institution) are set up in FundConnect.
Step: A JMeter script will execute the following steps: Log In à Start in Investor Home à Upload Multiple Orders à Select upload file à Submit file à Return to Investor Home.
Note: Each investor should upload a different file. The script should record the average time to execute the entire loop (Investor Home to Investor Home).
想必大家對這個(gè)需求再清楚不過(guò)了(畢竟剛完成測試工作)。用JMeter測試中有一個(gè)關(guān)鍵的組件(其余省略不說(shuō)了):
HttpRequest
Name:/fundconnect/FCUploadOrdersServlet
Send Parameters With the Request
Name value
thisURL /inv/upload_orders.jsp
submitURL /inv/upload_orders_summary.jsp
cancelURL /adm/index.jsp
Send a file with request
File name: D:\datatest\datatest\20order_upload\testdata\uploadorders_20_acct1.csv
Parameter Name: uploadfile
MIME Type: application/octet-stream
這個(gè)Request是向FCUploadOrdersServlet發(fā)出,其間傳遞了三個(gè)參數和一個(gè)文件,完成上傳order文件的工作。這個(gè)需求到此也就結束了,但大家有沒(méi)有想過(guò),如果把需求改成: 15 investors log into FundConnect via GUI. Each investor submits 20 orders via web page. 即如果要求這15個(gè)投資者通過(guò)web(非上傳文件方式)遞交20不同的order,模擬這樣的測試該如何進(jìn)行呢?JMeter針對這樣的Web頁(yè)面操作的測試,實(shí)現起來(lái)比較復雜。(如果誰(shuí)認為不對,可以聯(lián)系我,把你的方法告訴我)。我個(gè)人認為做Web Application頁(yè)面上的功能測試,使用下面談到的兩個(gè)框架,實(shí)現起來(lái)比較簡(jiǎn)單。
Web Application自動(dòng)化測試FrameWorks:HttpUnit JWebUnit
HttpUnit本身并沒(méi)有測試功能, 說(shuō)白了, 它不過(guò)包含了一些類(lèi)庫, 可以用來(lái)模擬出一個(gè)瀏覽器(WebConversation類(lèi))并可以模擬用戶(hù)在網(wǎng)頁(yè)上的多種行為。HttpUnit沒(méi)有測試的功能,所以它要結合JUnit來(lái)完成Web測試的工作,例如下面是一段簡(jiǎn)單的代碼:
import junit.framework.TestCase;
import com.meterware.httpunit.WebResponse;
import com.meterware.httpunit.WebConversation;
import com.meterware.httpunit.WebForm;
import com.meterware.httpunit.WebRequest;
public class SearchExample extends TestCase {
public void testSearch() throws Exception {
//模擬瀏覽器
WebConversation wc = new WebConversation();
//對google主頁(yè)發(fā)出request,并得到response
WebResponse resp = wc.getResponse( "http://www.google.com");
//從Response中抽取出第一個(gè)table
WebForm form = resp.getForms()[0];
//在搜索的text field called q中填寫(xiě)”HttpUnit”
form.setParameter("q", "HttpUnit");
//點(diǎn)擊提交按鈕
WebRequest req = form.getRequest("btnG");
resp = wc.getResponse(req);
//通過(guò)反饋回來(lái)的response來(lái)判斷叫做HttpUnit的link是否存在
assertNotNull(resp.getLinkWith("HttpUnit"));
//模擬點(diǎn)擊連接的功能
resp = resp.getLinkWith("HttpUnit").click();
//通過(guò)title來(lái)判斷返回的reponse的title是否為HttpUnit
assertEquals(resp.getTitle(), "HttpUnit");
assertNotNull(resp.getLinkWith("User‘s Manual"));
}
}
你可以發(fā)現上面的一個(gè)例子
, 已經(jīng)完成了對于在google中查找HttpUnit關(guān)鍵字的測試。
在此基礎上,JWebUnit更近一步,它實(shí)際上是建立在HttpUnit和JUnit框架之上,將二者功能結合、重構后的產(chǎn)物。同時(shí),JWebUnit提供了更加易用的API來(lái)模擬用戶(hù)對web界面的操作,同樣是上面的代碼,JWebUnit的實(shí)現如下:
import net.sourceforge.jwebunit.WebTestCase;
public class JWebUnitSearchExample extends WebTestCase {
public JWebUnitSearchExample(String name) {
super(name);
}
public void setUp() {
getTestContext().setBaseUrl("http://www.google.com");
}
public void testSearch() {
beginAt("/");
setFormElement("q", "httpunit");
submit("btnG");
clickLinkWithText("HttpUnit");
assertTitleEquals("HttpUnit");
assertLinkPresentWithText("User‘s Manual");
}
}
我相信不用加任何注釋, 大家也可以輕松的理解每一步操作。接下來(lái)我應用了JWebUnit測試了Fund Connect web頁(yè)面上的一些功能, 下面列出了以Admin身份登陸, 對增加Fund這樣一個(gè)功能的測試:
import net.sourceforge.jwebunit.WebTestCase;
import org.xml.sax.SAXException;
import com.meterware.httpunit.HttpUnitOptions;
import com.meterware.httpunit.WebConversation;
import com.meterware.httpunit.WebResponse;
public class AdminTest extends WebTestCase{
public static void main(String [] args){
junit.swingui.TestRunner.run(AdminTest.class);
}
public void setUp(){
//get rid of Java Script check
HttpUnitOptions.setExceptionsThrownOnScriptError(false);
//set the base url for this test
getTestContext().setBaseUrl("http://tcsunfire04.zdus.com:9220");
//set username and password to get through SiteMinder authentication
getTestContext().setAuthorization("bos.ssb.dwf", "123");
//set the cookie required
getTestContext().addCookie("SMCHALLENGE", "YES");
}
public void testSiteMinder(){
//test wether test can get through SiteMinder or not
beginAt("/fundconnect/adm");
assertTitleEquals("Global Link Fund Connect");
}
/**//*
*Test for adding a new Fund
*Fund long name: star‘s Fund
*Fund short name: starFund
*Fund Provider: Starhero
*/
public void testAddFund(){
beginAt("/fundconnect/adm/maintfunds_funddet.jsp?add=new");
//Fill in the add fund form
setFormElement("fundInstrumentLongName","star‘s fund");
setFormElement("fundInstrumentName","starFund");
setFormElement("fundInstrumentCode","123567");
selectOption("fundCodeType","ISO");
selectOption("timeZone","(GMT+08:00) Asia/Shanghai");
selectOption("fundProviderIndex","Starhero");
selectOption("settlementCurrency","USD -- US Dollar");
selectOption("partialShares","No");
setFormElement("contactName","Brooks");
setFormElement("contactPhoneNumber","13989472700");
selectOption("investmentType","Short Term");
selectOption("assetClass","EQUITY");
selectOption("industry","DEVELOPED");
selectOption("countryRegion","UNITED STATES");
selectOption("benchmark","AUD LIBID");
selectOption("domicileCountry","United States");
setFormElement("defaultPrice","50");
selectOption("fundInstrumentCountries","United States");
selectOption("institutionSelect","lon.ssb");
setFormElement("cutoff","22:00");
selectOption("cutoffType","Hard Cutoff");
//submit the form
360docimg_501_
360docimg_502_ submit("sbmtSubmit");
360docimg_503_
360docimg_504_ //According to the fund‘s long name and fund‘s short name to assert
360docimg_505_
360docimg_506_ //that fund is added
360docimg_507_
360docimg_508_ assertTextPresent("star‘s fund");
360docimg_509_
360docimg_510_ assertTextPresent("starFund");
360docimg_511_
360docimg_512_
360docimg_513_
360docimg_514_ }
360docimg_515_
360docimg_516_}
360docimg_517_
360docimg_518_
由此看出, JWebUnit可以完成Web頁(yè)面上復雜應用的測試??梢栽谝院蟮捻椖恐兄饾u使用。