不好意思,最近由于在趕項目所以這篇文章今天才有時間寫出來
首先講講taglib的使用目的,只有明確的使用目的我們才能寫出明確的單元測試
通常我們自定義的taglib都是為了根據一些參數達到我們需要view層樣式,在我的項目中一般比較少的使用自定義標簽的body形式(body一般是為了通過標簽達到框架級的頁面結構),因此,對于一個taglib來說它一般要做事情有:
1、獲取參數
2、根據參數獲取結果集(通常這個主要是bl層的任務)
3、根據結果集得到輸出樣式(得到的樣式一般都是一個html或者wml的字符串)
4、把得到的輸出樣式最終輸出到頁面上
根據上面的分析其實我們可以看出我們需要把測試的焦點集中在3上,因為其它的任務主要是通過調用其它封裝好的方法來實現的。
用一個實例來介紹一下我的做法吧:ShowCatalogTag,主要是根據傳遞的類別id,顯示相關的類別信息和子類信息。
根據需求我們不難看出這個標簽的主要功能是
1、獲取類別ID和相關樣式顯示參數
2、根據類別id獲取類別相關信息和子類別信息
3、根據類別信息結果集和顯示參數得到輸出wml代碼
4、把類別樣式最終輸出到頁面上
根據需求分析我們可以設計出我們標簽的主要方法有:
getOutWml()的到最終的wml輸出
getCatalogLayout(List catalogList)根據類別結果集得到類別樣式
然后,根據上述設計,我們可以首先寫我們的單元測試了
單元測試一般是從最底層的實現方法開始寫起,所以我們首先寫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
//得到待測方法結果
12
StringBuffer result = sct.getCatalogLayout(catalogList);
13
logger.debug(result);
14
//設置期望結果
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
//進行斷言判斷期望和實際結果
28
assertEquals(outPut.toString(),result.toString());
29
}
此時,有關getCatalogLayout的測試方法已經寫完了,但是實際上這個方法我們還沒有寫呢。所以在eclipse中會顯示錯誤我們使用eclipse的自動完成功能來在標簽中實現一個空getCatalogLayout方法,下面我將寫getCatalogLayout方法的實現 :

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,我們期待的綠條來了,如果要是紅條,那么你就需要仔細檢查一下你的方法實現代碼了:)
上面的方法其實主要是說了一下如何用tdd的方式來思考和編寫代碼,其實getCatalogLayout這個方法基本上是和標簽環境隔離的,需要傳遞的參數只有已知的cataloglist
下面將介紹一下在標簽中使用到相關環境時的解決方案
在標簽中我們一般需要使用的外部環境參數主要就是pageContext,而在pageContext中有我們需要的request,webapplicationcontext,response等等環境變量。
因此要進行完整的標簽單元測試就必須要考慮到把加入相關的環境變量
首先考慮加載spring的WebApplicationContext,前一篇文章講DAO單元測試的時候我提到過用springmock來加載spring的上下文,但是springmock加載的是ClassPathApplicationContext,兩者是不一樣的,而我查找了資料后沒有找到相關的轉換方法,結果我只有通過配置文件的路徑得到WebApplicationContext,下面是一個工具類用于對spring的相關信息進行加載

/** *//**
* $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設置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設置spring的上下文
* @param servletContext
*/

public static void setSpringContext(ServletContext servletContext)
{

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

}

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

/** *//**
* $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 設施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 根據默認路徑初始化Struts配置
*
*/

protected void strutsInit()
{

strutsInit("/index.do");
}


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

protected void strutsInit(String path)
{

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


/** *//**
* @author rocket 對于tag設施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加載模擬的環境信息:

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加載相關的struts和spring環境
setStrutsContextInTag(sct);
//得到實際結果
StringBuffer result = sct.getOutWml();
logger.debug(result);
//獲得期望結果
List catalogList = ChannelMGR.getCatalogsByChannelAndParentId(channel,
parentid);
//由于getCatalogLayout已經測試過,所以getCatalogLayout方法可以直接調用
StringBuffer expect = sct.getCatalogLayout(catalogList);
//斷言判斷
assertEquals(expect ,result );
}
為了簡化測試環境設置代碼,所以我在BaseTagTest中定義好了setStrutsContextInTag方法用于加載spring和struts的相關信息
下面針對getoutWml的測試我可以很快得到實現

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,然后得到一個漂亮的綠條。當你沒有得到green bar時首先要檢查的是你的測試邏輯和測試環境是否正確,然后再檢查你的實現代碼是否正確。
到此為止,我的ShowCatalogTag就已經開發完成了。有人也學會問怎么沒有沒有見到你的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的開發,總結來說,TDD的好處有:
1、開發時結構清晰,各個方法分工明確
2、各個方法目的明確,在實現之前我已經明確了這個方法的實現目的
3、開發快捷,以往開發taglib進行調試需要啟動web應用,但是在我這個開發過程中可以看到我并沒有啟動任何app應用
4、回歸測試,當需求發生變動時,要檢測變動是否會對已有功能造成影響,只需要執行以前的單元測試就可以了
當然,tdd的開發方式將會耗費大量的時間在外部環境的模擬上,我在模擬spring和struts的環境時就花費了比較久的時間在研究。不過,當我最后得到高質量的實現代碼時,我感覺到,這個代價是值得的
posted on 2007-02-06 17:46
rocket 閱讀(1555)
評論(2) 編輯 收藏