不好意思,最近由于在趕項目所以這篇文章今天才有時間寫出來
首先講講taglib的使用目的,只有明確的使用目的我們才能寫出明確的單元測試
通常我們自定義的taglib都是為了根據(jù)一些參數(shù)達到我們需要view層樣式,在我的項目中一般比較少的使用自定義標(biāo)簽的body形式(body一般是為了通過標(biāo)簽達到框架級的頁面結(jié)構(gòu)),因此,對于一個taglib來說它一般要做事情有:
1、獲取參數(shù)
2、根據(jù)參數(shù)獲取結(jié)果集(通常這個主要是bl層的任務(wù))
3、根據(jù)結(jié)果集得到輸出樣式(得到的樣式一般都是一個html或者wml的字符串)
4、把得到的輸出樣式最終輸出到頁面上
根據(jù)上面的分析其實我們可以看出我們需要把測試的焦點集中在3上,因為其它的任務(wù)主要是通過調(diào)用其它封裝好的方法來實現(xiàn)的。
用一個實例來介紹一下我的做法吧:ShowCatalogTag,主要是根據(jù)傳遞的類別id,顯示相關(guān)的類別信息和子類信息。
根據(jù)需求我們不難看出這個標(biāo)簽的主要功能是
1、獲取類別ID和相關(guān)樣式顯示參數(shù)
2、根據(jù)類別id獲取類別相關(guān)信息和子類別信息
3、根據(jù)類別信息結(jié)果集和顯示參數(shù)得到輸出wml代碼
4、把類別樣式最終輸出到頁面上
根據(jù)需求分析我們可以設(shè)計出我們標(biāo)簽的主要方法有:
getOutWml()的到最終的wml輸出
getCatalogLayout(List catalogList)根據(jù)類別結(jié)果集得到類別樣式
然后,根據(jù)上述設(shè)計,我們可以首先寫我們的單元測試了
單元測試一般是從最底層的實現(xiàn)方法開始寫起,所以我們首先寫testGetCatalogLayout
1
public void testGetCatalogLayout() throws Exception
{
4
List catalogList=new ArrayList();
5
CsCatalog testcatalog=new CsCatalog();
6
testcatalog.setCatalogName("ring");
7
testcatalog.setId(23l);
8
catalogList.add(testcatalog);
9
//得到待測方法結(jié)果
12
StringBuffer result = sct.getCatalogLayout(catalogList);
13
logger.debug(result);
14
//設(shè)置期望結(jié)果
15
StringBuffer outPut = new StringBuffer();
16
if (null != catalogList && catalogList.size() != 0)
{
17
CsCatalog catalog = (CsCatalog) catalogList.get(0);
18
Map parameterMap = new LinkedMap();
19
for (int i = 1; i < catalogList.size() - 1; i++)
{
20
21
catalog = (CsCatalog) catalogList.get(i);
22
parameterMap = new LinkedMap();
23
parameterMap.put("catalogid", Long.toString(catalog.getId()));
24
outPut.append(catalog.getCatalogName());
25
}
26
}
27
//進行斷言判斷期望和實際結(jié)果
28
assertEquals(outPut.toString(),result.toString());
29
}
此時,有關(guān)getCatalogLayout的測試方法已經(jīng)寫完了,但是實際上這個方法我們還沒有寫呢。所以在eclipse中會顯示錯誤我們使用eclipse的自動完成功能來在標(biāo)簽中實現(xiàn)一個空getCatalogLayout方法,下面我將寫getCatalogLayout方法的實現(xiàn) :

public StringBuffer getCatalogLayout(List catalogList)
{
StringBuffer outPut = new StringBuffer();

if (null != catalogList && catalogList.size() != 0)
{
CsCatalog catalog = (CsCatalog) catalogList.get(0);
Map parameterMap = new LinkedMap();

for (int i = 1; i < catalogList.size() - 1; i++)
{

catalog = (CsCatalog) catalogList.get(i);
parameterMap = new LinkedMap();
parameterMap.put("catalogid", Long.toString(catalog.getId()));
outPut.append(catalog.getCatalogName());
}
}
return outPut;
}
然后運行eclipse的run->junit test,ok,我們期待的綠條來了,如果要是紅條,那么你就需要仔細(xì)檢查一下你的方法實現(xiàn)代碼了:)
上面的方法其實主要是說了一下如何用tdd的方式來思考和編寫代碼,其實getCatalogLayout這個方法基本上是和標(biāo)簽環(huán)境隔離的,需要傳遞的參數(shù)只有已知的cataloglist
下面將介紹一下在標(biāo)簽中使用到相關(guān)環(huán)境時的解決方案
在標(biāo)簽中我們一般需要使用的外部環(huán)境參數(shù)主要就是pageContext,而在pageContext中有我們需要的request,webapplicationcontext,response等等環(huán)境變量。
因此要進行完整的標(biāo)簽單元測試就必須要考慮到把加入相關(guān)的環(huán)境變量
首先考慮加載spring的WebApplicationContext,前一篇文章講DAO單元測試的時候我提到過用springmock來加載spring的上下文,但是springmock加載的是ClassPathApplicationContext,兩者是不一樣的,而我查找了資料后沒有找到相關(guān)的轉(zhuǎn)換方法,結(jié)果我只有通過配置文件的路徑得到WebApplicationContext,下面是一個工具類用于對spring的相關(guān)信息進行加載

/** *//**
* $Id:$
*
* Copyright 2005 easou, Inc. All Rights Reserved.
*/
package test.spring.common;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.springframework.mock.web.MockPageContext;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;

import test.PathConfig;

import com.easou.commons.web.taglib.BaseTag;


public class SpringTestUtil
{


/** *//**
* @author rocket 初始化tag所需的MockServletContext
*
*/

protected static MockServletContext getSpringMockSC()
{
MockServletContext mockServletContext = new MockServletContext();
mockServletContext.addInitParameter(
ContextLoader.CONFIG_LOCATION_PARAM, PathConfig
.getStringXmlPath(PathConfig.springxml));
ServletContextListener listener = new ContextLoaderListener();
ServletContextEvent event = new ServletContextEvent(mockServletContext);
listener.contextInitialized(event);
return mockServletContext;

}


/** *//**
* @author rocket 針對tag設(shè)置Spring的上下文
* @param tag
*/

public static void setSpringContextInTag(BaseTag tag)
{

MockPageContext mockPC = new MockPageContext(getSpringMockSC());
tag.setPageContext(mockPC);
}


/** *//**
* @author rocket 獲得spring的上下文
* @return spring的上下文
*/

public static WebApplicationContext getSpringContext()
{
MockServletContext mockServletContext = getSpringMockSC();
return (WebApplicationContext) mockServletContext
.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);

}


/** *//**
* @author rocket 對servletContext設(shè)置spring的上下文
* @param servletContext
*/

public static void setSpringContext(ServletContext servletContext)
{

servletContext.setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
getSpringContext());
}

}

這個類中的幾個方法可以對mock的pagecontext或者servletcontext加載spring的上下文
這里簡單介紹一下我使用的環(huán)境模擬工具:
我沒有使用流行的mockObject和MockRunner,主要是因為我的項目基本框架是spring+Struts,而對這兩個框架的模擬有更有針對性地springmock和strutstestcase,這樣在我需要加載struts相關(guān)配置信息時,strutstestcase提供了更好的支持。
比如我這里需要使用到strust中配置好的properties信息,那么下面各測試基類就基本實現(xiàn)的我的要求

/** *//**
* $Id:$
*
* Copyright 2005 easou, Inc. All Rights Reserved.
*/
package test.struts.common;

import org.springframework.mock.web.MockPageContext;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.web.context.WebApplicationContext;

import servletunit.struts.MockStrutsTestCase;
import test.spring.common.SpringTestUtil;

import com.easou.commons.web.taglib.BaseTag;


public class BaseStrutsTestCase extends MockStrutsTestCase
{


/** *//** The transaction manager to use */
protected PlatformTransactionManager transactionManager;


/** *//**
* TransactionStatus for this test. Typical subclasses won't need to use it.
*/
protected TransactionStatus transactionStatus;


/** *//**
* @author rocket 設(shè)施struts配置文件的路徑
*
*/

protected void setUp() throws Exception
{
super.setUp();
setConfigFile("/WEB-INF/struts-config.xml");
}


protected void tearDown() throws Exception
{
super.tearDown();
WebApplicationContext wac = (WebApplicationContext) context
.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
transactionManager = (PlatformTransactionManager) wac
.getBean("transactionManager");
transactionStatus = transactionManager
.getTransaction(new DefaultTransactionDefinition());
transactionManager.rollback(this.transactionStatus);
logger.info("Rolled back transaction after test execution");
}


/** *//**
* @author rocket 根據(jù)默認(rèn)路徑初始化Struts配置
*
*/

protected void strutsInit()
{

strutsInit("/index.do");
}


/** *//**
* @author rocket 根據(jù)路徑初始化Struts配置
* @param path
*/

protected void strutsInit(String path)
{

setRequestPathInfo(path);
actionPerform();
SpringTestUtil.setSpringContext(context);
}


/** *//**
* @author rocket 對于tag設(shè)施struts的配置 同時加載spring的配置
* @param tag
*/

protected void setStrutsContextInTag(BaseTag tag)
{
strutsInit();

MockPageContext mockPC = new MockPageContext(context,request,response);
tag.setPageContext(mockPC);
}


/** *//**
* @author rocket 測試struts加載是否成功
*
*/

public void testStrutsInit()
{
strutsInit();
verifyNoActionErrors();
verifyForward("success");
assertTrue("struts has bean init", isInitialized);
assertNotNull("ServletContext is not null", context);
assertNotNull("request is not null", request);
}

}

下面回到我們的ShowCatalogTag中來,我要開始對getOutWml進行測試了,在測試這個方法之前我們需要對ShowCatalogTag加載模擬的環(huán)境信息:

public class ShowCatalogsTagTest extends BaseTagTest
{
ShowCatalogsTag sct=new ShowCatalogsTag();

public void testGetOutWml() throws Exception
{
String channel="Ring";
String parentid = "-1";
sct.setChannel(channel);
sct.setHerf("/channel/RingCatalogRcPage.do");
sct.setParentid(parentid);
sct.setRowCount("2");
sct.setSplitchar("$");
sct.setLongName("false");
sct.setFirstBracket("false");
sct.setFirstHerf(null);
//這里針對tag加載相關(guān)的struts和spring環(huán)境
setStrutsContextInTag(sct);
//得到實際結(jié)果
StringBuffer result = sct.getOutWml();
logger.debug(result);
//獲得期望結(jié)果
List catalogList = ChannelMGR.getCatalogsByChannelAndParentId(channel,
parentid);
//由于getCatalogLayout已經(jīng)測試過,所以getCatalogLayout方法可以直接調(diào)用
StringBuffer expect = sct.getCatalogLayout(catalogList);
//斷言判斷
assertEquals(expect ,result );
}
為了簡化測試環(huán)境設(shè)置代碼,所以我在BaseTagTest中定義好了setStrutsContextInTag方法用于加載spring和struts的相關(guān)信息
下面針對getoutWml的測試我可以很快得到實現(xiàn)

public StringBuffer getOutWml()
{
log.debug("channel" + channel);
log.debug("parentid" + parentid);
ChannelMGR = (ChannelManager) getBean("ChannelManager");
List catalogList = ChannelMGR.getCatalogsByChannelAndParentId(channel,
parentid);
StringBuffer outPut = getCatalogLayout(catalogList);
return outPut;
}
后面的事情就是我們最期待得run->junit test,然后得到一個漂亮的綠條。當(dāng)你沒有得到green bar時首先要檢查的是你的測試邏輯和測試環(huán)境是否正確,然后再檢查你的實現(xiàn)代碼是否正確。
到此為止,我的ShowCatalogTag就已經(jīng)開發(fā)完成了。有人也學(xué)會問怎么沒有沒有見到你的doStartTag這個方法呢,是這樣的我封裝了一個Basetag定義了公用的方法


protected Object getBean(String beanName)
{
ctx = WebApplicationContextUtils
.getRequiredWebApplicationContext(pageContext
.getServletContext());
return ctx.getBean(beanName);
}

protected MessageResources getResources(HttpServletRequest request,

String key)
{
ServletContext context = pageContext.getServletContext();
ModuleConfig moduleConfig = ModuleUtils.getInstance().getModuleConfig(
request, context);
return (MessageResources) context.getAttribute(key
+ moduleConfig.getPrefix());
}


public int doStartTag() throws JspException
{
initManager();

try
{
this.pageContext.getOut().write(getOutWml().toString());

} catch (IOException ex)
{
log.error(ex);
}
return this.SKIP_BODY;
}
以上的過程就是我使用TDD的方法來進行一個taglib的開發(fā),總結(jié)來說,TDD的好處有:
1、開發(fā)時結(jié)構(gòu)清晰,各個方法分工明確
2、各個方法目的明確,在實現(xiàn)之前我已經(jīng)明確了這個方法的實現(xiàn)目的
3、開發(fā)快捷,以往開發(fā)taglib進行調(diào)試需要啟動web應(yīng)用,但是在我這個開發(fā)過程中可以看到我并沒有啟動任何app應(yīng)用
4、回歸測試,當(dāng)需求發(fā)生變動時,要檢測變動是否會對已有功能造成影響,只需要執(zhí)行以前的單元測試就可以了
當(dāng)然,tdd的開發(fā)方式將會耗費大量的時間在外部環(huán)境的模擬上,我在模擬spring和struts的環(huán)境時就花費了比較久的時間在研究。不過,當(dāng)我最后得到高質(zhì)量的實現(xiàn)代碼時,我感覺到,這個代價是值得的
posted on 2007-02-06 17:46
rocket 閱讀(1556)
評論(2) 編輯 收藏