Java系列--第五篇 基于Maven的SSME之Token及Parameterized单元测试
本来在第四篇要说完的,但是写着写着,我觉得内容有点多起来了,所以就另开这篇,在这里专门讲述Token的定义,JSP自定义标签以及如何用Parameterized的来做单元测试。
1,新建包com.vanceinfo.javaserial.handlerinterceptors,新加类TokenHandler,这个类就是Token的Helper类了,包含三个方法:
generateGUID当进入页面加载时,产生一个GUID,分别存入Session和Constant里,说明一下,Constant是用于页面的hidden值保存用的。。。。,顺便打个预防针的是sesseion里面存的是map对象,使用的key叫SPRINGMVC.TOKEN,map里面的一条对象以"springMVC_token.GUID:GUID"形式保存。而客户端的hidden框的name使用的是小写的springMVC_token
getInputToken获取客户端hidden里面的guid值。
validToken这个方法用于验证客户端Hidden里的guid里值,与服务端Session里面对应的值是否一致,完全相同而返回true,否则返回false, 并且先会remove掉session里面的对应的这条token值。
源码如下:
package com.vanceinfo.javaserial.handlerinterceptors; import java.math.BigInteger; import java.util.HashMap; import java.util.Map; import java.util.Random; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.apache.log4j.Logger; import org.springframework.ui.ModelMap; import com.vanceinfo.javaserial.constants.Constant; public class TokenHandler { private static Logger LOGGER = Logger.getLogger(TokenHandler.class); static Map<String, String> springmvc_token = new HashMap<String, String>(); /** * generate the unique token, and store into both server, client side. * * @param session * @return */ @SuppressWarnings("unchecked") public synchronized static String generateGUID(HttpSession session, ModelMap map) { String token = ""; try { Object obj = session.getAttribute("SPRINGMVC.TOKEN"); if (obj != null) { springmvc_token = (Map<String, String>) session.getAttribute("SPRINGMVC.TOKEN"); } token = new BigInteger(165, new Random()).toString(36).toUpperCase(); springmvc_token.put(Constant.DEFAULT_TOKEN_NAME + "." + token, token); session.setAttribute("SPRINGMVC.TOKEN", springmvc_token); Constant.TOKEN_VALUE = token; } catch (IllegalStateException e) { LOGGER.error("generateGUID() mothod find bug,by token session..."); } return token; } /** * validate the form token value and session token value. * * @param request * @return true if both token value are the same,otherwise false */ @SuppressWarnings("unchecked") public static boolean validToken(HttpServletRequest request) { String inputToken = getInputToken(request); if (inputToken == null) { LOGGER.warn("token is not valid!inputToken is NULL"); return false; } HttpSession session = request.getSession(); Map<String, String> tokenMap = (Map<String, String>) session.getAttribute("SPRINGMVC.TOKEN"); if (tokenMap == null || tokenMap.size() < 1) { LOGGER.warn("token is not valid!sessionToken is NULL"); return false; } String sessionToken = tokenMap.get(Constant.DEFAULT_TOKEN_NAME + "." + inputToken); if (!inputToken.equals(sessionToken)) { LOGGER.warn("token is not valid!inputToken='" + inputToken + "',sessionToken = '" + sessionToken + "'"); return false; } tokenMap.remove(Constant.DEFAULT_TOKEN_NAME + "." + inputToken); session.setAttribute("SPRINGMVC.TOKEN", tokenMap); return true; } /** * Get the token value from the form. assume it store in the hidden field * * @param request * @return */ @SuppressWarnings("unchecked") public static String getInputToken(HttpServletRequest request) { Map<String, String[]> params = request.getParameterMap(); if (!params.containsKey(Constant.DEFAULT_TOKEN_NAME)) { LOGGER.warn("Could not find token name in params."); return null; } String[] tokens = (String[]) (String[]) params.get(Constant.DEFAULT_TOKEN_NAME); if ((tokens == null) || (tokens.length < 1)) { LOGGER.warn("Got a null or empty token name."); return null; } return tokens[0]; } }
通过上面的描述,聪明你可以很自然的猜到Constant.java里面会有两个常量值
public static String DEFAULT_TOKEN_NAME = "springMVC_token"; public static String TOKEN_VALUE;
Interceptor里面其实编写代码起来要容易的多,实现HandlerInterceptor接口,其实主要是在preHandler和postHandler就好了。
/* (non-Javadoc) * @see org.springframework.web.servlet.HandlerInterceptor#postHandle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object, org.springframework.web.servlet.ModelAndView) */ public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { if ("get".equalsIgnoreCase(request.getMethod())) { TokenHandler.generateGUID(request.getSession(), modelAndView.getModelMap()); } } /* (non-Javadoc) * @see org.springframework.web.servlet.HandlerInterceptor#preHandle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object) */ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("get".equalsIgnoreCase(request.getMethod())) { return true; } else { if (!TokenHandler.validToken(request)) { response.sendRedirect("index.do"); return false; } return true; } }
以上代码是接合我的一些情况,如果二次请求,我是跳转至index.do这个方法的。
2, 将刚写的这个interceptor注册至spring-mvc.xml里面。
<mvc:interceptors> <!-- use Token to void multi post when user press F5 or Ctrol+F5 --> <mvc:interceptor> <mvc:mapping path="/**/*.do" /> <bean class="com.vanceinfo.javaserial.handlerinterceptors.TokenHandlerInterceptor" /> </mvc:interceptor> </mvc:interceptors>
3, 接下来就是要写tag了。4步实现自定义tag:
a,先定义一个BaseTag,让其继承于TagSupport,这是最简单的实现方式
package com.vanceinfo.javaserial.tags; import javax.servlet.jsp.tagext.TagSupport; public class BaseTag extends TagSupport { private static final long serialVersionUID = -4886769810825854364L; protected String name; protected String type; protected String id; protected String onclick; protected String onfocus; protected String onblur; protected String onchange; protected String cssStyle; protected String cssClass; protected String size; public void generateAttribute(StringBuilder sb) { if (id != null) { sb.append(" id='").append(id).append("'"); } if (onclick != null) { sb.append(" onclick='").append(onclick).append("'"); } if (onfocus != null) { sb.append(" onfocus='").append(onfocus).append("'"); } if (onblur != null) { sb.append(" onblur='").append(onblur).append("'"); } if (onchange != null) { sb.append(" onchange='").append(onchange).append("'"); } if (cssStyle != null) { sb.append(" style='").append(cssStyle).append("'"); } if (cssClass != null) { sb.append(" class='").append(cssClass).append("'"); } if (size != null) { sb.append(" size='").append(size).append("'"); } } /** * @return the name */ public String getName() { return name; } /** * @param name the name to set */ public void setName(String name) { this.name = name; } /** * @return the type */ public String getType() { return type; } /** * @param type the type to set */ public void setType(String type) { this.type = type; } /** * @return the id */ public String getId() { return id; } /** * @param id the id to set */ public void setId(String id) { this.id = id; } /** * @return the onclick */ public String getOnclick() { return onclick; } /** * @param onclick the onclick to set */ public void setOnclick(String onclick) { this.onclick = onclick; } /** * @return the onfocus */ public String getOnfocus() { return onfocus; } /** * @param onfocus the onfocus to set */ public void setOnfocus(String onfocus) { this.onfocus = onfocus; } /** * @return the onblur */ public String getOnblur() { return onblur; } /** * @param onblur the onblur to set */ public void setOnblur(String onblur) { this.onblur = onblur; } /** * @return the onchange */ public String getOnchange() { return onchange; } /** * @param onchange the onchange to set */ public void setOnchange(String onchange) { this.onchange = onchange; } /** * @return the cssStyle */ public String getCssStyle() { return cssStyle; } /** * @param cssStyle the cssStyle to set */ public void setCssStyle(String cssStyle) { this.cssStyle = cssStyle; } /** * @return the cssClass */ public String getCssClass() { return cssClass; } /** * @param cssClass the cssClass to set */ public void setCssClass(String cssClass) { this.cssClass = cssClass; } /** * @return the size */ public String getSize() { return size; } /** * @param size the size to set */ public void setSize(String size) { this.size = size; } }
自定义的TokenTag 继承于BaseTag
package com.vanceinfo.javaserial.tags; import java.io.IOException; import javax.servlet.jsp.JspException; import com.vanceinfo.javaserial.constants.Constant; public class TokenTag extends BaseTag { private static final long serialVersionUID = 1495609370076247263L; /* (non-Javadoc) * @see javax.servlet.jsp.tagext.TagSupport#doStartTag() */ @Override public int doStartTag() throws JspException { StringBuilder sb = new StringBuilder(); sb.append("<input type='text' name='").append( Constant.DEFAULT_TOKEN_NAME).append("'").append(" value='").append(Constant.TOKEN_VALUE).append("'"); sb.append(" /> "); try { pageContext.getOut().print(sb.toString()); } catch (IOException e) { e.printStackTrace(); } return EVAL_BODY_INCLUDE; } }
b,接着就去WEB-INF下面创建标签库描述文件,注意这个文件只能放在这个文件夹下,当然为了区分,你可以在其下自建一个文件夹,至于放其它地方会出错的原因还不明。
<?xml version="1.0" encoding="UTF-8"?> <taglib version="2.0" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee web-jsptaglibrary_2_0.xsd"> <tlib-version>1.0</tlib-version> <jsp-version>2.0</jsp-version> <short-name>token</short-name> <uri>/mytaglib</uri> <tag> <name>token</name> <tag-class>com.vanceinfo.javaserial.tags.TokenTag</tag-class> <body-content>empty</body-content> <attribute> <name>user</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> </tag> </taglib>
至于c、d两步,我在第四篇中已经讲述,故不在重复,不清楚的可以返回上篇查阅一下。
以上就是关于自定义类Struts2的Token的完整过程,Struts2是经历了很多人的检验的,我们的自造的,为了表示出我们的信心和诚意,自然对我们的Interceptor进行单元测试喽。当然,从浏览器现象来看个现象也是好的,所谓有图有真相有说服力
4,关于单元测试,我们利用Parameterized参数化集中测试,没弄这种单元测试之前,我也很悚这种模式,不过一来二去之后,对其还是持拥抱态度的。其实,单元测试针对的是单元,看你对单元的单位是如何定义的,一般来讲一个方法就是一个单元来测最合适。因为我是这样子认同的,所以,对于每一个方法,我们需要创建一个JUnit4的Test Case.并且勾选setUpBeforeClass,tearDownAfterClass,setUp,tearDown,这4个方法一般用于记一些描述性步骤等内容,下面是TokenHandlerInterceptorTest_postHandleTest里的一个示例。
/** * @throws java.lang.Exception */ @BeforeClass public static void setUpBeforeClass() throws Exception { LOGGER.debug("Starting test class : " + TokenHandlerInterceptorTest_postHandleTest.class.getName()); } /** * @throws java.lang.Exception */ @AfterClass public static void tearDownAfterClass() throws Exception { LOGGER.debug("Ending test class : " + TokenHandlerInterceptorTest_postHandleTest.class.getName()); } /** * @throws java.lang.Exception */ @Before public void setUp() throws Exception { LOGGER.debug("Starting test: " + testName); } /** * @throws java.lang.Exception */ @After public void tearDown() throws Exception { LOGGER.debug("Ending test: " + testName); }
还有一个约定成俗的定义一个变量,用于对每次执行一时输出你的test case名字出来。
private String testName;
除了这个约定以外,如果你的测试方法的入参也请列在private列表里面,作为变量,也就是参数。最后,测试方法的返回值也请以参数的形式定义。这一次,我举TokenHandlerInterceptor_preHandleTest作为例子,因为他是有返回值的。
private String testName; private HttpServletRequest httpRequest; private HttpServletResponse httpResponse; private boolean returnBoolean;
然后,你还要定义一个带所有参数的构造函数出来。例如:
public TokenHandlerInterceptor_preHandleTest(String testName, HttpServletRequest httpRequest, HttpServletResponse httpResponse, Boolean returnBoolean) { this.testName = testName; this.httpRequest = httpRequest; this.httpResponse = httpResponse; this.returnBoolean = returnBoolean; }
最后,准备测试数据,写在testData()函数里面,一定要以这个命名,返回Collection<Object[]>,并且打上@org.junit.runners.Parameterized.Parameters标签,
准备完这些之后中,在class前面也一定不要忘了打上@RunWith(Parameterized.class)标签。
一个完整的TokenHandlerInterceptor_preHandleTest如下:
package com.vanceinfo.javaserial.handlerinterceptors; import static org.junit.Assert.*; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.log4j.Logger; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; @RunWith(Parameterized.class) public class TokenHandlerInterceptor_preHandleTest { private static final Logger LOGGER = Logger.getLogger(TokenHandlerInterceptor_preHandleTest.class); private String testName; private HttpServletRequest httpRequest; private HttpServletResponse httpResponse; private boolean returnBoolean; public TokenHandlerInterceptor_preHandleTest(String testName, HttpServletRequest httpRequest, HttpServletResponse httpResponse, Boolean returnBoolean) { this.testName = testName; this.httpRequest = httpRequest; this.httpResponse = httpResponse; this.returnBoolean = returnBoolean; } @Parameters public static Collection<Object[]> testData() throws Exception { // ******************* // test case 1 // ******************* String testNameTC1 = "Get Method"; MockHttpServletRequest httpRequest1 = new MockHttpServletRequest("get", "http://testRequestUrl"); httpRequest1.addParameter("htid", "1001"); MockHttpServletResponse httpResponse1 = new MockHttpServletResponse(); // ******************* // test case 2 // ******************* String testNameTC2 = "Post Method unvalid: no param(client) token"; MockHttpServletRequest httpRequest2 = new MockHttpServletRequest("post", "http://testRequestUrl"); httpRequest2.addParameter("htid", "1001"); MockHttpServletResponse httpResponse2 = new MockHttpServletResponse(); // ******************* // test case 3 // ******************* String testNameTC3 = "Post Method unvalid: has param(client) token but is null"; MockHttpServletRequest httpRequest3 = new MockHttpServletRequest("post", "http://testRequestUrl"); httpRequest3.addParameter("htid", "1001"); httpRequest3.addParameter("springMVC_token", new String[] {}); MockHttpServletResponse httpResponse3 = new MockHttpServletResponse(); // ******************* // test case 4 // ******************* String testNameTC4 = "Post Method unvalid: has param(client) token but no session token"; MockHttpServletRequest httpRequest4 = new MockHttpServletRequest("post", "http://testRequestUrl"); httpRequest4.addParameter("htid", "1001"); httpRequest4.addParameter("springMVC_token", new String[] { "abcdef" }); MockHttpServletResponse httpResponse4 = new MockHttpServletResponse(); // ******************* // test case 5 // ******************* String testNameTC5 = "Post Method unvalid: has param(client) token and session token,but not the same value"; MockHttpServletRequest httpRequest5 = new MockHttpServletRequest("post", "http://testRequestUrl"); httpRequest5.addParameter("htid", "1001"); httpRequest5.addParameter("springMVC_token", new String[] { "abcdef" }); MockHttpSession session5 = new MockHttpSession(); Map<String, String> tokenMap5 = new HashMap<String, String>(); tokenMap5.put("springMVC_token.abcdef", "abcdefg"); session5.putValue("SPRINGMVC.TOKEN", tokenMap5); httpRequest5.setSession(session5); MockHttpServletResponse httpResponse5 = new MockHttpServletResponse(); // ******************* // test case 6 // ******************* String testNameTC6 = "Post Method valid"; MockHttpServletRequest httpRequest6 = new MockHttpServletRequest("post", "http://testRequestUrl"); httpRequest6.addParameter("htid", "1001"); httpRequest6.addParameter("springMVC_token", new String[] { "abcdef" }); MockHttpSession session6 = new MockHttpSession(); Map<String, String> tokenMap6 = new HashMap<String, String>(); tokenMap6.put("springMVC_token.abcdef", "abcdef"); session6.putValue("SPRINGMVC.TOKEN", tokenMap6); httpRequest6.setSession(session6); MockHttpServletResponse httpResponse6 = new MockHttpServletResponse(); return Arrays .asList(new Object[][] { { testNameTC1, httpRequest1, httpResponse1, true }, { testNameTC2, httpRequest2, httpResponse2, false }, { testNameTC3, httpRequest3, httpResponse3, false }, { testNameTC4, httpRequest4, httpResponse4, false }, { testNameTC5, httpRequest5, httpResponse5, false }, { testNameTC6, httpRequest6, httpResponse6, true } }); } /** * @throws java.lang.Exception */ @BeforeClass public static void setUpBeforeClass() throws Exception { LOGGER.debug("Starting test class : " + TokenHandlerInterceptor_preHandleTest.class.getName()); } /** * @throws java.lang.Exception */ @AfterClass public static void tearDownAfterClass() throws Exception { LOGGER.debug("Ending test class : " + TokenHandlerInterceptor_preHandleTest.class.getName()); } /** * @throws java.lang.Exception */ @Before public void setUp() throws Exception { LOGGER.debug("Starting test: " + testName); } /** * @throws java.lang.Exception */ @After public void tearDown() throws Exception { LOGGER.debug("Ending test: " + testName); } /** * Test method for * {@link com.expedia.lux.drr.handlerinterceptors.TokenHandlerInterceptor#preHandle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object)} * . * * @throws Exception */ @Test public void testPreHandle() { TokenHandlerInterceptor pre = new TokenHandlerInterceptor(); try { boolean actual = pre.preHandle(httpRequest, httpResponse, new Object()); assertEquals(this.returnBoolean, actual); } catch (Exception e) { fail("Should not throw exception!"); } } }
注意我在上面设计了6个case. 大部分期望的是返回false,也就是负case的测试。而assert 的时候,只需写一次就可以了。
这就是参数化单元测试的好处了。耗时就耗在准备数据上面。
希望把自定义标签的制作说明白了。