本文通过TDD设计模式实现一个简易的struts框架。
一. 什么是TDD设计模式?
TDD是测试驱动开发(Test-Driven Development)的英文简称,是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD虽是敏捷方法的核心实践,但不只适用于XP(Extreme Programming),同样可以适用于其他开发方法和过程。
TDD的基本思路就是通过测试来推动整个开发的进行,它以不断的测试推动代码的开发,既简化了代码,又保证了软件质量,测试驱动开发技术并不只是单纯的测试工作。
二.设计需求
本文设计了一个lite-struts,主要功能为在网页登陆界面(login),输入用户名(name)及密码(password),如果与后台预设的用户名(name)和密码(password)一致,则返回成功的信息(success),并打开相应的页面(成功情况下的jsp);如果失败,则返回失败的信息(fail),并打开相应的页面(失败情况下的jsp)。在网页登出界面(logout),与登陆界面思想一样,分为成功(success)和错误(error)两种响应,并打开相对应的界面。
具体的功能需求如下struts.xml文件所示:
<?xml version="1.0" encoding="UTF-8"?>
<struts>
<action name="login" class="com.coderising.litestruts.LoginAction">
<result name="success">/jsp/homepage.jsp</result>
<result name="fail">/jsp/showLogin.jsp</result>
</action>
<action name="logout" class="com.coderising.litestruts.LogoutAction">
<result name="success">/jsp/welcome.jsp</result>
<result name="error">/jsp/error.jsp</result>
</action>
</struts>
三.设计思路
采用先写测试用例再写主要功能代码的TDD模式进行驱动设计,本文主要分析登陆的代码设计,登出的思想和登陆一样,就不赘述。
整体设计思路如下:
- 第一步:解析xml。
- 第二步:根据action的name确定对应的class。
- 第三步:通过反射获取所有的set方法,并调用set方法将相应的name和password存入到Map中(key-value对,name=?,password=?,即登陆时输入的name和password)。
- 第四步:调用方法(本文中为exeute方法)确定name和password是否正确,正确返回信息success,错误则返回信息fail。
- 第五步:通过反射获取所有的get方法,反射调用并把值和属性形成一个HashMap,例如:{“name”,“test”},{“password”,“123456”}{“message”,“login successful.”}(如果name和password均正确,则会得到message为login successful.),并存入到视图文件(本文为类View)的Map数据段中(本文为parameters)。
- 第六步:通过action的name以及第四步返回的提示信息(success/fail)进行判断,确定相对应的jsp页面,并存储到视图文件的jsp中。
- 第七步:返回View。即通过传入的actionName以及存有name和password的值的parameters(Map类型),从第一步到第六步确定了相应的视图View,最后返回得到相应的试图View,视图View中主要存有jsp和相应的message提示(本文用到这两项进行junit测试)。
1.第一个测试用例:统揽全局,上帝视角分析核心需求。
当我们进入登陆界面时,传入的是login,此时需要输入name和password以验证登陆,如果均正确,则打开成功的jsp页面,并提示成功;如果验证失败,则提示失败并留在登陆界面以继续输入name和password。核心测试代码如下,该测试若能通过,则我们的任务就全部完成了。
package com.coderising.litestruts;
import java.util.HashMap;
import java.util.Map;
import org.junit.Assert;
import org.junit.Test;
public class StrutsTest {
@Test
public void testLoginActionSuccess() {
String actionName = "login";
Map<String,String> params = new HashMap<String,String>();
params.put("name","test");
params.put("password","1234");
View view = Struts.runAction(actionName,params);
Assert.assertEquals("/jsp/homepage.jsp", view.getJsp());
Assert.assertEquals("login successful", view.getParameters().get("message"));
}
@Test
public void testLoginActionFailed() {
String actionName = "login";
Map<String,String> params = new HashMap<String,String>();
params.put("name","test");
params.put("password","123456"); //密码和预设的不一致
View view = Struts.runAction(actionName,params);
Assert.assertEquals("/jsp/showLogin.jsp", view.getJsp());
Assert.assertEquals("login failed,please check your user/pwd", view.getParameters().get("message"));
}
}
此时类Struts和类View以及这两个类下有一些方法出现了红线提示错误,这时我们采用IDE自动补全代码的方式创建这两个类以及相应的方法。
先写核心类struts.java,我们所有的各模块核心功能代码都会在这个类中组装以完成我们最终的lite-struts。类struts骨架代码如下:
package com.coderising.litestruts;
import java.lang.reflect.Method;
import java.util.Map;
public class Struts {
public static View runAction(String actionName, Map<String,String> parameters) {
return null;
}
}
核心骨架代码很简单,runAction方法中第一个参数为传入一个actionName字符串以判断action是login还是logout,我们这主要测试login,logout类似处理。第二个参数采用Map传入name和password以验证正确性,最后返回对应的View视图。这个骨架一目了然,我们先把它放在这里不用管,后面通过一步步TDD设计逼近最终功能,最后再来补全核心代码。
我们还需要一个视图文件,以返回正确的对应视图。相关代码如下:
package com.coderising.litestruts;
import java.util.Map;
public class View {
private String jsp;
private Map parameters;
public String getJsp() {
return jsp;
}
public View setJsp(String jsp) {
this.jsp = jsp;
return this;
}
public Map getParameters() {
return parameters;
}
public View setParameters(Map parameters) {
this.parameters = parameters;
return this;
}
}
我们这里还需要一个登陆文件以用来设置和获取name和password,以及验证name和password正确性的方法(在此登陆文件中设置好了正确的name和password),以用来提供success或fail的提示信息。相关代码如下:
package com.coderising.litestruts;
public class LoginAction{
private String name ;
private String password;
private String message;
public String getName() {
return name;
}
public String getPassword() {
return password;
}
public String execute(){
if("test".equals(name) && "1234".equals(password)){
this.message = "login successful";
return "success";
}
this.message = "login failed,please check your user/pwd";
return "fail";
}
public void setName(String name){
this.name = name;
}
public void setPassword(String password){
this.password = password;
}
public String getMessage(){
return this.message;
}
}
2.第二个测试用例:解析xml,获取xml到java的反射。
首先,我们需要解析xml文件,并通过传入的action的name是login还是logout来获取相应的class文件。我们的思想是先把xml文件传入一个配置文件(此处用的Configuration.java)中,并将其解析,然后通过传入"login"或"logout"参数得到与其相对应的class名。测试代码如下所示:
@Test
public void testGetClassName() {
Configuration cfg = new Configuration("struts.xml");
String clzName = cfg.getClassName("login");
Assert.assertEquals("com.coderising.litestruts.LoginAction", clzName);
clzName = cfg.getClassName("logout");
Assert.assertEquals("com.coderising.litestruts.LogoutAction", clzName);
}
此时Configuration类和getClassName方法下会出现红线提示错误,这时只需根据下面的提示新建Configuration类和相应的getClassName方法就可以了,建好之后红线就会消失。然后就把新建好的Configuration类的构造函数和getClassName方法写好即可。
然后,我们需要根据登陆(login)或登出(logout)以及其操作后得到的提示信息(成功或失败/错误)来确定需要获取哪个jsp页面,具体的测试代码如下:
@Test
public void testGetResultView(){
Configuration cfg = new Configuration("struts.xml");
String jsp = cfg.getResultView("login","success");
Assert.assertEquals("/jsp/homepage.jsp", jsp);
jsp = cfg.getResultView("login","fail");
Assert.assertEquals("/jsp/showLogin.jsp", jsp);
jsp = cfg.getResultView("logout","success");
Assert.assertEquals("/jsp/welcome.jsp", jsp);
jsp = cfg.getResultView("logout","error");
Assert.assertEquals("/jsp/error.jsp", jsp);
}
同样,我们把红线提示错误的getResultView方法加入到Configuration类中,并把该获取相应jsp的功能代码写好。
此时,第二个测试用例完成了,并在相对应的功能代码中完成了相关设计。
第二个测试用例完整代码如下:
package com.coderising.litestruts;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class ConfigurationTest {
Configuration cfg = new Configuration("struts.xml");
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void testGetClassName() {
String clzName = cfg.getClassName("login");
Assert.assertEquals("com.coderising.litestruts.LoginAction", clzName);
clzName = cfg.getClassName("logout");
Assert.assertEquals("com.coderising.litestruts.LogoutAction", clzName);
}
@Test
public void testGetResultView(){
String jsp = cfg.getResultView("login","success");
Assert.assertEquals("/jsp/homepage.jsp", jsp);
jsp = cfg.getResultView("login","fail");
Assert.assertEquals("/jsp/showLogin.jsp", jsp);
jsp = cfg.getResultView("logout","success");
Assert.assertEquals("/jsp/welcome.jsp", jsp);
jsp = cfg.getResultView("logout","error");
Assert.assertEquals("/jsp/error.jsp", jsp);
}
}
第二个测试用例相对应的解析xml,获取相对应的class以及jsp的完整功能代码如下:
package com.coderising.litestruts;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
public class Configuration {
Map<String,ActionConfig> actions = new HashMap<>();
public Configuration(String fileName) {
String packageName = this.getClass().getPackage().getName();
packageName = packageName.replace('.', '/');
InputStream is = this.getClass().getResourceAsStream("/" + packageName + "/" + fileName);
parseXML(is);
try {
is.close();
} catch (IOException e) {
throw new ConfigurationException(e);
}
}
private void parseXML(InputStream is){
SAXBuilder builder = new SAXBuilder();
try {
Document doc = builder.build(is);
Element root = doc.getRootElement();
for(Element actionElement : root.getChildren("action")){
String actionName = actionElement.getAttributeValue("name");
String clzName = actionElement.getAttributeValue("class");
ActionConfig ac = new ActionConfig(actionName, clzName);
for(Element resultElement : actionElement.getChildren("result")){
String resultName = resultElement.getAttributeValue("name");
String viewName = resultElement.getText().trim();
ac.addViewResult(resultName, viewName);
}
this.actions.put(actionName, ac);
}
} catch (JDOMException e) {
throw new ConfigurationException(e);
} catch (IOException e) {
throw new ConfigurationException(e);
}
}
public String getClassName(String action) {
ActionConfig ac = this.actions.get(action);
if(ac == null){
return null;
}
return ac.getClassName();
}
public String getResultView(String action, String resultName) {
ActionConfig ac = this.actions.get(action);
if(ac == null){
return null;
}
return ac.getViewName(resultName);
}
private static class ActionConfig{
String name;
String clzName;
Map<String,String> viewResult = new HashMap<>();
public ActionConfig(String actionName, String clzName) {
this.name = actionName;
this.clzName = clzName;
}
public String getClassName(){
return clzName;
}
public void addViewResult(String name, String viewName){
viewResult.put(name, viewName);
}
public String getViewName(String resultName){
return viewResult.get(resultName);
}
}
}
此处采用了jdom解析xml文件,需要导入相对应的jar包。
相对应的异常处理代码:
package com.coderising.litestruts;
import java.io.IOException;
import org.jdom2.JDOMException;
public class ConfigurationException extends RuntimeException {
public ConfigurationException(String msg) {
super(msg);
}
public ConfigurationException(JDOMException e) {
super(e);
}
public ConfigurationException(IOException e) {
super(e);
}
}
3.第三个测试用例:通过反射操作对象。
首先,我们需要通过反射获得所有的set方法,以用来设置name和password(即登陆时输入的name和password),测试代码如下:
@Test
public void testGetSetterMethod() throws Exception {
String name = "com.coderising.litestruts.LoginAction";
Class<?> clz = Class.forName(name);
List<Method> methods = ReflectionUtil.getSetterMethods(clz);
Assert.assertEquals(2, methods.size());
List<String> expectedNames = new ArrayList<>();
expectedNames.add("setName");
expectedNames.add("setPassword");
Set<String> acctualNames = new HashSet<>();
for(Method m : methods){
acctualNames.add(m.getName());
}
Assert.assertTrue(acctualNames.containsAll(expectedNames));
}
根据提示创建RefletionUtil类和相应的getSetterMethods方法,完成相应的代码设计。
反射得到了所有的set方法之后,我们需要调用set方法来设置name和password,并把name=?,password=?这样的key-value对存储到Map中。相关测试代码如下:
@Test
public void testSetParameters() throws Exception{
String name = "com.coderising.litestruts.LoginAction";
Class<?> clz = Class.forName(name);
Object o = clz.newInstance();
Map<String,String> params = new HashMap<String,String>();
params.put("name","test");
params.put("password","1234");
ReflectionUtil.setParameters(o,params);
Field f = clz.getDeclaredField("name");
f.setAccessible(true);
Assert.assertEquals("test", f.get(o));
f = clz.getDeclaredField("password");
f.setAccessible(true);
Assert.assertEquals("1234", f.get(o));
}
根据提示创建RefletionUti类和相应的setParameters方法,完成相应的代码设计。
反射获取所有的get方法,相关测试代码如下:
@Test
public void testGetGetterMethod() throws Exception{
String name = "com.coderising.litestruts.LoginAction";
Class<?> clz = Class.forName(name);
List<Method> methods = ReflectionUtil.getGetterMethods(clz);
Assert.assertEquals(3, methods.size());
List<String> expectedNames = new ArrayList<>();
expectedNames.add("getMessage");
expectedNames.add("getName");
expectedNames.add("getPassword");
Set<String> acctualNames = new HashSet<>();
for(Method m : methods){
acctualNames.add(m.getName());
}
Assert.assertTrue(acctualNames.containsAll(expectedNames));
}
根据提示创建RefletionUti类和相应的getGetterMethods方法,完成相应的代码设计。
通过反射获取所有的get方法后,反射调用get方法以获取一一对应的name和password以及message的值,相关测试代码如下:
@Test
public void testGetParamters() throws Exception{
String name = "com.coderising.litestruts.LoginAction";
Class<?> clz = Class.forName(name);
LoginAction action = (LoginAction)clz.newInstance();
action.setName("test");
action.setPassword("123456");
Map<String,Object> params = ReflectionUtil.getParamterMap(action);
Assert.assertEquals(3, params.size());
Assert.assertEquals(null, params.get("messaage") );
Assert.assertEquals("test", params.get("name") );
Assert.assertEquals("123456", params.get("password") );
}
根据提示创建RefletionUti类和相应的getParamterMap方法,完成相应的代码设计。
第三个测试用例完整代码如下:
package com.coderising.litestruts;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class ReflectionUtilTest {
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void testGetSetterMethod() throws Exception {
String name = "com.coderising.litestruts.LoginAction";
Class<?> clz = Class.forName(name);
List<Method> methods = ReflectionUtil.getSetterMethods(clz);
Assert.assertEquals(2, methods.size());
List<String> expectedNames = new ArrayList<>();
expectedNames.add("setName");
expectedNames.add("setPassword");
Set<String> acctualNames = new HashSet<>();
for(Method m : methods){
acctualNames.add(m.getName());
}
Assert.assertTrue(acctualNames.containsAll(expectedNames));
}
@Test
public void testSetParameters() throws Exception{
String name = "com.coderising.litestruts.LoginAction";
Class<?> clz = Class.forName(name);
Object o = clz.newInstance();
Map<String,String> params = new HashMap<String,String>();
params.put("name","test");
params.put("password","1234");
ReflectionUtil.setParameters(o,params);
Field f = clz.getDeclaredField("name");
f.setAccessible(true);
Assert.assertEquals("test", f.get(o));
f = clz.getDeclaredField("password");
f.setAccessible(true);
Assert.assertEquals("1234", f.get(o));
}
@Test
public void testGetGetterMethod() throws Exception{
String name = "com.coderising.litestruts.LoginAction";
Class<?> clz = Class.forName(name);
List<Method> methods = ReflectionUtil.getGetterMethods(clz);
Assert.assertEquals(3, methods.size());
List<String> expectedNames = new ArrayList<>();
expectedNames.add("getMessage");
expectedNames.add("getName");
expectedNames.add("getPassword");
Set<String> acctualNames = new HashSet<>();
for(Method m : methods){
acctualNames.add(m.getName());
}
Assert.assertTrue(acctualNames.containsAll(expectedNames));
}
@Test
public void testGetParamters() throws Exception{
String name = "com.coderising.litestruts.LoginAction";
Class<?> clz = Class.forName(name);
LoginAction action = (LoginAction)clz.newInstance();
action.setName("test");
action.setPassword("123456");
Map<String,Object> params = ReflectionUtil.getParamterMap(action);
Assert.assertEquals(3, params.size());
Assert.assertEquals(null, params.get("messaage") );
Assert.assertEquals("test", params.get("name") );
Assert.assertEquals("123456", params.get("password") );
}
}
第三个测试用例对应的功能代码如下:
package com.coderising.litestruts;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ReflectionUtil {
public static List<Method> getSetterMethods(Class clz) {
return getMethods(clz,"set");
}
public static void setParameters(Object o, Map<String, String> params) {
List<Method> methods = getSetterMethods(o.getClass());
for(String name : params.keySet() ){
String methodName = "set" + name;
for(Method m: methods){
if(m.getName().equalsIgnoreCase(methodName)){
try {
m.invoke(o, params.get(name));
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
}
public static List<Method> getGetterMethods(Class<?> clz) {
return getMethods(clz,"get");
}
private static List<Method> getMethods(Class<?> clz, String startWithName){
List<Method> methods = new ArrayList<>();
for(Method m : clz.getDeclaredMethods()){
if(m.getName().startsWith(startWithName)){
methods.add(m);
}
}
return methods;
}
public static Map<String, Object> getParamterMap(Object o) {
Map<String ,Object> params = new HashMap<>();
List<Method> methods = getGetterMethods(o.getClass());
for(Method m : methods){
String methodName = m.getName();
String name = methodName.replaceFirst("get", "").toLowerCase();
try {
Object value = m.invoke(o);
params.put(name, value);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
}
}
return params;
}
Backup ///
public static List<Method> getGetterMethods_V1(Class<?> clz) {
List<Method> methods = new ArrayList<>();
for(Method m : clz.getDeclaredMethods()){
if(m.getName().startsWith("get")){
methods.add(m);
}
}
return methods;
}
public static List<Method> getSetterMethods_V1(Class clz) {
List<Method> methods = new ArrayList<>();
for(Method m : clz.getDeclaredMethods()){
if(m.getName().startsWith("set")){
methods.add(m);
}
}
return methods;
}
}
此处采用了代码重构,getGetterMethods方法和getSetterMethods方法除了"get"和"set"这一点区别外完全一致(如上述代码Backup分界符下面的代码所示,方法名后面加了_V1以防止重名错误),所以采用重构把不同之处(字符串"get"和"set")以第二个方法参数传入新的getMethods方法中,原来的getGetterMethods方法和getSetterMethods方法只需直接return并新传入字符串方法参数"get"或"set"即可,这样代码变得会更加简洁。
最后,我们只需把最开始的核心骨架代码struts.java补全即可,代码如下所示:
package com.coderising.litestruts;
import java.lang.reflect.Method;
import java.util.Map;
public class Struts {
private final static Configuration cfg = new Configuration("struts.xml");
public static View runAction(String actionName, Map<String,String> parameters) {
String clzName = cfg.getClassName(actionName);
if(clzName == null){
return null;
}
try {
Class<?> clz = Class.forName(clzName);
Object action = clz.newInstance();
ReflectionUtil.setParameters(action, parameters);
Method m = clz.getDeclaredMethod("execute");
String resultName = (String)m.invoke(action);
Map<String,Object> params = ReflectionUtil.getParamterMap(action);
String resultView = cfg.getResultView(actionName, resultName);
View view = new View();
view.setParameters(params);
view.setJsp(resultView);
return view;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
此时,运行最开始的StrutsTest.java测试文件,通过即完成所有的测试。
如上图,StrutsTest测试通过,即我们的简易struts就大功告成了。
特别注意:在上述各个模块的单元测试中,每一个方法测试(@Test)在写完相对应的一个方法后,例如:第二个测试用例的第一个测试testGetClassName(),测试通过action的name得到相对应的class,在对应的类Configuration中写完getClassName()方法后,即运行测试已验证该方法是否书写正确。其他的设计都是一样,写一个@Test,写一个方法,运行测试进行验证该方法的正确性,以此来逼近最终要实现的功能。本文直接给出了最后的三个测试用例运行成功的图,但是按一个@Test写一个对应的方法就进行测试验证一步一步走到最后的,需谨记,这才是TDD设计模式的核心思想。