不好意思,最近由于在赶项目所以这篇文章今天才有时间写出来
 
首先讲讲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
public void testGetCatalogLayout() throws Exception  {
{
 4 List catalogList=new ArrayList();
        List catalogList=new ArrayList();
 5 CsCatalog testcatalog=new CsCatalog();
        CsCatalog testcatalog=new CsCatalog();
 6 testcatalog.setCatalogName("ring");
        testcatalog.setCatalogName("ring");
 7 testcatalog.setId(23l);
        testcatalog.setId(23l);
 8 catalogList.add(testcatalog);
        catalogList.add(testcatalog);
 9 //得到待测方法结果
         //得到待测方法结果
12 StringBuffer result = sct.getCatalogLayout(catalogList);
        StringBuffer result = sct.getCatalogLayout(catalogList);
13 logger.debug(result);
        logger.debug(result);
14 //设置期望结果
        //设置期望结果
15 StringBuffer outPut = new StringBuffer();
        StringBuffer outPut = new StringBuffer();
16
 if (null != catalogList && catalogList.size() != 0)
        if (null != catalogList && catalogList.size() != 0)  {
{
17 CsCatalog catalog = (CsCatalog) catalogList.get(0);
            CsCatalog catalog = (CsCatalog) catalogList.get(0);
18 Map parameterMap = new LinkedMap();
            Map parameterMap = new LinkedMap();            
19
 for (int i = 1; i < catalogList.size() - 1; i++)
            for (int i = 1; i < catalogList.size() - 1; i++)  {
{
20
21 catalog = (CsCatalog) catalogList.get(i);
                catalog = (CsCatalog) catalogList.get(i);
22 parameterMap = new LinkedMap();
                parameterMap = new LinkedMap();
23 parameterMap.put("catalogid", Long.toString(catalog.getId()));
                parameterMap.put("catalogid", Long.toString(catalog.getId()));
24 outPut.append(catalog.getCatalogName());
                outPut.append(catalog.getCatalogName());                
25 }
            }    
26 }
        }
27 //进行断言判断期望和实际结果
        //进行断言判断期望和实际结果
28 assertEquals(outPut.toString(),result.toString());
        assertEquals(outPut.toString(),result.toString());
29 }
    } 
此时,有关getCatalogLayout的测试方法已经写完了,但是实际上这个方法我们还没有写呢。所以在eclipse中会显示错误我们使用eclipse的自动完成功能来在标签中实现一个空getCatalogLayout方法,下面我将写getCatalogLayout方法的实现 : 

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

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

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

 catalog = (CsCatalog) catalogList.get(i);
                catalog = (CsCatalog) catalogList.get(i);
 parameterMap = new LinkedMap();
                parameterMap = new LinkedMap();
 parameterMap.put("catalogid", Long.toString(catalog.getId()));
                parameterMap.put("catalogid", Long.toString(catalog.getId()));
 outPut.append(catalog.getCatalogName());
                outPut.append(catalog.getCatalogName());                
 }
            }
 }
        }        
 return outPut;
        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:$
 * $Id:$
 *
 *
 * Copyright 2005 easou, Inc. All Rights Reserved.
 * Copyright 2005 easou, Inc. All Rights Reserved.
 */
 */
 package test.spring.common;
package test.spring.common;

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

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

 import test.PathConfig;
import test.PathConfig;

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


 public class SpringTestUtil
public class SpringTestUtil  {
{


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

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

 }
    }


 /** *//**
    /** *//**
 * @author rocket 针对tag设置Spring的上下文
     * @author rocket 针对tag设置Spring的上下文
 * @param tag
     * @param tag
 */
     */

 public static void setSpringContextInTag(BaseTag tag)
    public static void setSpringContextInTag(BaseTag tag)  {
{

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


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

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

 }
    }


 /** *//**
    /** *//**
 * @author rocket 对servletContext设置spring的上下文
     * @author rocket 对servletContext设置spring的上下文
 * @param servletContext
     * @param servletContext
 */
     */

 public static void setSpringContext(ServletContext servletContext)
    public static void setSpringContext(ServletContext servletContext)  {
{

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

 }
}

这个类中的几个方法可以对mock的pagecontext或者servletcontext加载spring的上下文
这里简单介绍一下我使用的环境模拟工具:
我没有使用流行的mockObject和MockRunner,主要是因为我的项目基本框架是spring+Struts,而对这两个框架的模拟有更有针对性地springmock和strutstestcase,这样在我需要加载struts相关配置信息时,strutstestcase提供了更好的支持。
比如我这里需要使用到strust中配置好的properties信息,那么下面各测试基类就基本实现的我的要求

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

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

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

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


 public class BaseStrutsTestCase extends MockStrutsTestCase
public class BaseStrutsTestCase extends MockStrutsTestCase  {
{


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


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


 /** *//**
    /** *//**
 * @author rocket 设施struts配置文件的路径
     * @author rocket 设施struts配置文件的路径
 *
     * 
 */
     */

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


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


 /** *//**
    /** *//**
 * @author rocket 根据默认路径初始化Struts配置
     * @author rocket 根据默认路径初始化Struts配置
 *
     * 
 */
     */

 protected void strutsInit()
    protected void strutsInit()  {
{

 strutsInit("/index.do");
        strutsInit("/index.do");
 }
    }


 /** *//**
    /** *//**
 * @author rocket 根据路径初始化Struts配置
     * @author rocket 根据路径初始化Struts配置
 * @param path
     * @param path
 */
     */

 protected void strutsInit(String path)
    protected void strutsInit(String path)  {
{

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


 /** *//**
    /** *//**
 * @author rocket 对于tag设施struts的配置 同时加载spring的配置
     * @author rocket 对于tag设施struts的配置 同时加载spring的配置
 * @param tag
     * @param tag
 */
     */

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

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


 /** *//**
    /** *//**
 * @author rocket 测试struts加载是否成功
     * @author rocket 测试struts加载是否成功
 *
     * 
 */
     */

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

 }
}

下面回到我们的ShowCatalogTag中来,我要开始对getOutWml进行测试了,在测试这个方法之前我们需要对ShowCatalogTag加载模拟的环境信息:

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

 public void testGetOutWml() throws Exception
    public void testGetOutWml() throws Exception  {
{
 
        
 String channel="Ring";
        String channel="Ring";
 String parentid = "-1";
        String parentid = "-1";
 sct.setChannel(channel);
        sct.setChannel(channel);
 sct.setHerf("/channel/RingCatalogRcPage.do");
        sct.setHerf("/channel/RingCatalogRcPage.do");
 sct.setParentid(parentid);
        sct.setParentid(parentid);
 sct.setRowCount("2");
        sct.setRowCount("2");
 sct.setSplitchar("$");
        sct.setSplitchar("$");
 sct.setLongName("false");
        sct.setLongName("false");
 sct.setFirstBracket("false");
        sct.setFirstBracket("false");
 sct.setFirstHerf(null);
        sct.setFirstHerf(null);
 //这里针对tag加载相关的struts和spring环境
        //这里针对tag加载相关的struts和spring环境
 setStrutsContextInTag(sct);
        setStrutsContextInTag(sct);
 //得到实际结果
        //得到实际结果
 StringBuffer result = sct.getOutWml();
        StringBuffer result = sct.getOutWml();
 logger.debug(result);
        logger.debug(result);
 //获得期望结果
        //获得期望结果
 List catalogList = ChannelMGR.getCatalogsByChannelAndParentId(channel,
        List catalogList = ChannelMGR.getCatalogsByChannelAndParentId(channel,
 parentid);
                parentid);
 //由于getCatalogLayout已经测试过,所以getCatalogLayout方法可以直接调用
        //由于getCatalogLayout已经测试过,所以getCatalogLayout方法可以直接调用
 StringBuffer expect = sct.getCatalogLayout(catalogList);
        StringBuffer expect = sct.getCatalogLayout(catalogList);     
            //断言判断
           assertEquals(expect ,result );
 }
    }为了简化测试环境设置代码,所以我在BaseTagTest中定义好了setStrutsContextInTag方法用于加载spring和struts的相关信息
 下面针对getoutWml的测试我可以很快得到实现

 public StringBuffer getOutWml()
public StringBuffer getOutWml()  {
{        
 log.debug("channel" + channel);
        log.debug("channel" + channel);
 log.debug("parentid" + parentid);
        log.debug("parentid" + parentid);
 ChannelMGR = (ChannelManager) getBean("ChannelManager");
        ChannelMGR = (ChannelManager) getBean("ChannelManager");
 List catalogList = ChannelMGR.getCatalogsByChannelAndParentId(channel,
        List catalogList = ChannelMGR.getCatalogsByChannelAndParentId(channel,
 parentid);
                parentid);
 StringBuffer outPut = getCatalogLayout(catalogList);
        StringBuffer outPut = getCatalogLayout(catalogList);    
 return outPut;
        return outPut;
 }
    }后面的事情就是我们最期待得run->junit test,然后得到一个漂亮的绿条。当你没有得到green bar时首先要检查的是你的测试逻辑和测试环境是否正确,然后再检查你的实现代码是否正确。
到此为止,我的ShowCatalogTag就已经开发完成了。有人也学会问怎么没有没有见到你的doStartTag这个方法呢,是这样的我封装了一个Basetag定义了公用的方法


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

 protected MessageResources getResources(HttpServletRequest request,
    protected MessageResources getResources(HttpServletRequest request,

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


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

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

 } catch (IOException ex)
        } catch (IOException ex)  {
{
 log.error(ex);
            log.error(ex);
 }
        }
 return this.SKIP_BODY;
        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 阅读(1579) 
评论(2)  编辑  收藏